diff --git a/SCHEMA_DELTAS.md b/SCHEMA_DELTAS.md index b467ed6c1..0f0db1c28 100644 --- a/SCHEMA_DELTAS.md +++ b/SCHEMA_DELTAS.md @@ -1,174 +1,3 @@ # Generated-types delta -## Field changes - -- `bundled/content_standards/get_media_buy_artifacts_request.py` - - **classes removed**: DataSubjectContestation37 -- `bundled/core/tasks_get_response.py` - - **classes added**: ActivationKey8, Adjustment, GeoProximityItem, GeoProximityItem3, IncludedSignal, IncludedSignal1, IncludedSignal2, IncludedSignal3, Placement, Placement2, Placement3, Placement4, PricingOption171, PricingOption172, PricingOption173, PricingOption174, PricingOption175, PricingOption176, PricingOption18, PricingOption19, PricingOption20, Signal21, Signal22, Signal23, Signal24, Signal25, Signal26, Signal27, Signal31, Signal32, Signal33, Signal34, Signal35, Signal36, Signal37, SignalId40, SignalId7, SignalRef8, SignalRef9, SignalTargeting4, SignalTargeting6, SignalTargeting8, TargetFrequency2, Type33 - - **classes removed**: ActivationKey28, Adjustments, Adjustments3, AudienceSize1, AudienceTargeting6, AudienceTargeting7, AudienceTargeting8, DataSubjectContestation71, FormatOptions113, FormatOptions114, FormatOptions115, FormatOptions116, FormatOptions117, FormatOptions118, FormatOptions119, FormatOptions120, FormatOptions121, FormatOptions122, FormatOptions123, FormatOptions124, FormatOptions125, FormatOptions127, FormatOptions128, FormatOptions129, FormatOptions130, FormatOptions131, FormatOptions132, FormatOptions133, FormatOptions134, FormatOptions135, FormatOptions136, FormatOptions137, FormatOptions138, FormatOptions139, FormatOptions141, FormatOptions142, FormatOptions143, FormatOptions144, FormatOptions145, FormatOptions146, FormatOptions147, FormatOptions148, FormatOptions149, FormatOptions150, FormatOptions151, FormatOptions152, FormatOptions153, FormatOptions155, FormatOptions156, FormatOptions157, FormatOptions158, FormatOptions159, FormatOptions160, FormatOptions161, FormatOptions162, FormatOptions163, FormatOptions164, FormatOptions165, FormatOptions166, FormatOptions167, FrequencyCap4, FrequencyCap5, FrequencyCap6, FrequencyCap7, FrequencyCap8, FrequencyCap9, GeoProximity, GeoProximity10, GeoProximity11, GeoProximity12, GeoProximity13, GeoProximity9, IncludedSignals, IncludedSignals1, IncludedSignals2, IncludedSignals3, IncludedSignals4, IncludedSignals5, IncludedSignals6, IncludedSignals7, Metrics2, Metrics3, Metrics4, Metrics5, Metrics6, Params121, Params122, Params123, Params124, Params125, Params126, Params127, Params128, Params129, Params130, Params131, Params132, Params133, Params134, Params135, Params136, Params137, Params138, Params139, Params140, Params141, Params142, Params143, Params144, Params145, Params146, Params147, Params148, Params149, Params150, Params151, Params152, Params153, Params154, Params155, Params156, Params157, Params158, Params159, Params160, Params161, Params162, Params163, Params164, Params165, Params166, Params167, Params168, Placements, Placements1, Placements2, Placements3, Placements4, Placements5, Placements6, Placements7, PriceBreakdown10, PriceBreakdown11, PriceBreakdown12, PriceBreakdown13, PriceBreakdown14, PriceBreakdown15, PriceBreakdown16, PriceBreakdown17, PriceBreakdown18, PriceBreakdown19, PriceBreakdown2, PriceBreakdown20, PriceBreakdown21, PriceBreakdown22, PriceBreakdown23, PriceBreakdown24, PriceBreakdown25, PriceBreakdown26, PriceBreakdown27, PriceBreakdown28, PriceBreakdown29, PriceBreakdown3, PriceBreakdown30, PriceBreakdown31, PriceBreakdown32, PriceBreakdown33, PriceBreakdown34, PriceBreakdown35, PriceBreakdown36, PriceBreakdown37, PriceBreakdown38, PriceBreakdown4, PriceBreakdown5, PriceBreakdown6, PriceBreakdown7, PriceBreakdown8, PriceBreakdown9, PricingOption221, PricingOption222, PricingOption223, PricingOption224, PricingOption225, PricingOption226, PricingOption23, PricingOption24, PricingOption25, PricingOption26, PricingOption27, PricingOption28, PricingOption29, PricingOption30, PricingOption31, PricingOption32, PricingOption33, Signal1, Signal2, Signal4, Signal5, Signal6, Signal7, Signal8, Signal81, Signal82, Signal83, Signal84, Signal85, Signal86, Signal87, SignalId104, SignalId29, SignalRef41, SignalRef42, SignalTargeting12, SignalTargeting13, SignalTargeting14, SignalTargeting15, SignalTargeting16, SignalTargeting17, SignalTargeting18, SignalTargeting19, SignalTargeting20, SignalTargetingOption4, SignalTargetingOption5, SignalTargetingOption6, SignalTargetingOption61, SignalTargetingOption62, SignalTargetingOption63, SignalTargetingOption64, SignalTargetingOption65, SignalTargetingOption7, SignalTargetingOption71, SignalTargetingOption72, SignalTargetingOption73, SignalTargetingOption74, SignalTargetingOption75, SignalTargetingOption8, SignalTargetingOption81, SignalTargetingOption82, SignalTargetingOption83, SignalTargetingOption84, SignalTargetingOption85, TargetFrequency3, TargetFrequency4, TargetFrequency5, Type41 - - `AudienceTargeting3`: `+values` `-value` - - `AudienceTargeting4`: `+max_value`, `+min_value` `-values` - - `AudienceTargeting5`: `+category`, `+description` `-signal_id`, `-signal_ref`, `-value_type`, `-values` - - `Signal3`: `+root` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` - - `SignalTargeting11`: `+max_value`, `+min_value` `-values` - - `SignalTargeting7`: `+max_value`, `+min_value`, `+signal_id`, `+signal_ref`, `+value_type` `-root` - - `SignalTargetingOption`: `+activation_status`, `+allowed_targeting_modes`, `+categories`, `+default_selected`, `+description`, `+last_updated`, `+methodology_url`, `+name`, `+pricing_options`, `+range`, `+selection_group`, `+signal_agent_segment_id`, `+signal_id`, `+signal_ref`, `+value_type` `-root` - - `SignalTargetingOption1`: `+activation_status`, `+allowed_targeting_modes`, `+default_selected`, `+pricing_options`, `+selection_group`, `+signal_agent_segment_id` - - `SignalTargetingOption2`: `+activation_status`, `+allowed_targeting_modes`, `+default_selected`, `+pricing_options`, `+selection_group`, `+signal_agent_segment_id` - - `Type37`: `+MultiPolygon`, `+Polygon` `-fixed_fee`, `-full_commitment`, `-none`, `-percent_remaining` -- `bundled/creative/get_creative_delivery_request.py` - - **classes removed**: DataSubjectContestation17 -- `bundled/creative/get_creative_delivery_response.py` - - **classes removed**: DataSubjectContestation19 -- `bundled/creative/get_creative_features_request.py` - - **classes removed**: DataSubjectContestation31 -- `bundled/creative/list_creative_formats_request.py` - - **classes removed**: DataSubjectContestation47 -- `bundled/creative/list_creative_formats_response.py` - - **classes added**: PricingOption141, PricingOption142, PricingOption143, PricingOption144, PricingOption145, PricingOption146 - - **classes removed**: PricingOption191, PricingOption192, PricingOption193, PricingOption194, PricingOption195, PricingOption196 -- `bundled/creative/list_creatives_request.py` - - **classes removed**: DataSubjectContestation49 -- `bundled/creative/list_creatives_response.py` - - **classes added**: PricingOption151, PricingOption152, PricingOption153, PricingOption154, PricingOption155, PricingOption156 - - **classes removed**: DataSubjectContestation53, PricingOption201, PricingOption202, PricingOption203, PricingOption204, PricingOption205, PricingOption206 -- `bundled/creative/preview_creative_request.py` - - **classes removed**: DataSubjectContestation61 -- `bundled/creative/sync_creatives_request.py` - - **classes removed**: DataSubjectContestation69 -- `bundled/creative/validate_input_request.py` - - **classes removed**: DataSubjectContestation147 -- `bundled/media_buy/build_creative_request.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/media_buy/create_media_buy_request.py` - - **classes added**: GeoProximityItem, Type1, Unit5, Unit6 - - **classes removed**: DataSubjectContestation1, FrequencyCap1, FrequencyCap2, GeoProximity, GeoProximity1, GeoProximity2, SignalTargeting4, SignalTargeting5, SignalTargeting6, TargetFrequency1, Type3, Unit10, Unit11 - - `Signal4`: `+activation_key`, `+pricing_option_id`, `+signal_agent_segment_id` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` - - `SignalTargeting2`: `+values` `-value` - - `SignalTargeting3`: `+max_value`, `+min_value` `-values` -- `bundled/media_buy/get_media_buy_delivery_request.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/media_buy/get_media_buy_delivery_response.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/media_buy/get_media_buys_request.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/media_buy/get_media_buys_response.py` - - **classes added**: GeoProximityItem, Unit2, Unit3 - - **classes removed**: DataSubjectContestation1, FrequencyCap1, FrequencyCap2, GeoProximity, GeoProximity1, GeoProximity2, SignalTargeting4, SignalTargeting5, SignalTargeting6, Unit6, Unit7 - - `Signal4`: `+activation_key`, `+pricing_option_id`, `+signal_agent_segment_id` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` - - `SignalTargeting2`: `+values` `-value` - - `SignalTargeting3`: `+max_value`, `+min_value` `-values` -- `bundled/media_buy/get_products_request.py` - - **classes added**: GeoProximityItem, RequiredVendorMetric, SignalTargetingItem7, Unit2 - - **classes removed**: BrandKitOverride4, BudgetRange1, DataSubjectContestation1, Disclosure4, EmbeddedProvenanceItem4, GeoProximity, GeoProximity1, GeoProximity2, Logo4, Provenance4, RequiredVendorMetrics, RequiredVendorMetrics1, Unit6, Vendor2, Watermark4 - - `SignalTargetingItem`: `+root` `-targeting_mode` - - `SignalTargetingItem2`: `+values` `-value` - - `SignalTargetingItem3`: `+max_value`, `+min_value` `-values` - - `SignalTargetingItem4`: `+targeting_mode` `-signal_id`, `-signal_ref`, `-value_type`, `-values` - - `SignalTargetingItem5`: `+targeting_mode` `-max_value`, `-min_value`, `-signal_id`, `-signal_ref`, `-value_type` - - `SignalTargetingItem6`: `+targeting_mode` `-max_value`, `-min_value`, `-signal_id`, `-signal_ref`, `-value_type` -- `bundled/media_buy/get_products_response.py` - - **classes added**: Adjustment, IncludedSignal, IncludedSignal1, Mode1, Placement, Placement1, SignalId8 - - **classes removed**: Adjustments, Adjustments1, AudienceSize1, DataSubjectContestation1, FormatOptions56, FormatOptions57, FormatOptions58, FormatOptions59, FormatOptions60, FormatOptions61, FormatOptions62, FormatOptions63, FormatOptions64, FormatOptions65, FormatOptions66, FormatOptions67, FormatOptions68, FormatOptions70, FormatOptions71, FormatOptions72, FormatOptions73, FormatOptions74, FormatOptions75, FormatOptions76, FormatOptions77, FormatOptions78, FormatOptions79, FormatOptions80, FormatOptions81, FormatOptions82, IncludedSignals, IncludedSignals1, IncludedSignals2, IncludedSignals3, Metrics1, Metrics2, Metrics3, Mode2, Params48, Params49, Params50, Params51, Params52, Params53, Params54, Params55, Params56, Params57, Params58, Params59, Params60, Params61, Params62, Params63, Params64, Params65, Params66, Params67, Params68, Params69, Params70, Params71, Placements, Placements1, Placements2, Placements3, PriceBreakdown1, PriceBreakdown10, PriceBreakdown11, PriceBreakdown12, PriceBreakdown13, PriceBreakdown14, PriceBreakdown15, PriceBreakdown16, PriceBreakdown17, PriceBreakdown2, PriceBreakdown3, PriceBreakdown4, PriceBreakdown5, PriceBreakdown6, PriceBreakdown7, PriceBreakdown8, PriceBreakdown9, PricingOption13, PricingOption14, PricingOption15, PricingOption16, SignalId20, SignalTargetingOption2, SignalTargetingOption3, SignalTargetingOption4, SignalTargetingOption5, SignalTargetingOption6, SignalTargetingOption61, SignalTargetingOption62, SignalTargetingOption63, SignalTargetingOption64, SignalTargetingOption65 - - `SignalTargetingOption`: `+activation_status`, `+allowed_targeting_modes`, `+categories`, `+default_selected`, `+description`, `+last_updated`, `+methodology_url`, `+name`, `+pricing_options`, `+range`, `+selection_group`, `+signal_agent_segment_id`, `+signal_id`, `+signal_ref`, `+value_type` `-root` - - `SignalTargetingOption1`: `+activation_status`, `+allowed_targeting_modes`, `+default_selected`, `+pricing_options`, `+selection_group`, `+signal_agent_segment_id` -- `bundled/media_buy/log_event_request.py` - - **classes removed**: UserMatch1, UserMatch2, UserMatch3, UserMatch4 -- `bundled/media_buy/package_request.py` - - **classes added**: GeoProximityItem, Type1, Unit5, Unit6 - - **classes removed**: DataSubjectContestation1, FrequencyCap1, FrequencyCap2, GeoProximity, GeoProximity1, GeoProximity2, SignalTargeting4, SignalTargeting5, SignalTargeting6, TargetFrequency1, Type3, Unit10, Unit11 - - `Signal4`: `+activation_key`, `+pricing_option_id`, `+signal_agent_segment_id` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` - - `SignalTargeting2`: `+values` `-value` - - `SignalTargeting3`: `+max_value`, `+min_value` `-values` -- `bundled/media_buy/sync_audiences_request.py` - - **classes added**: AddItem - - **classes removed**: Add, Add1, Add2, DataSubjectContestation1 -- `bundled/media_buy/sync_catalogs_request.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/media_buy/sync_event_sources_request.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/media_buy/update_media_buy_request.py` - - **classes added**: GeoProximityItem, GeoProximityItem1 - - **classes removed**: DataSubjectContestation1, FrequencyCap2, FrequencyCap3, FrequencyCap4, FrequencyCap5, GeoProximity, GeoProximity1, GeoProximity2, GeoProximity3, GeoProximity4, GeoProximity5, SignalTargeting10, SignalTargeting11, SignalTargeting12, SignalTargeting13, SignalTargeting8, SignalTargeting9, TargetFrequency2, TargetFrequency3 - - `Signal4`: `+activation_key`, `+pricing_option_id`, `+signal_agent_segment_id` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` - - `Signal84`: `+activation_key`, `+pricing_option_id`, `+signal_agent_segment_id` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` - - `SignalTargeting2`: `+values` `-value` - - `SignalTargeting3`: `+max_value`, `+min_value` `-values` - - `SignalTargeting4`: `+root` `-signal_id`, `-signal_ref`, `-value_type`, `-values` - - `SignalTargeting5`: `+value` `-max_value`, `-min_value` - - `SignalTargeting6`: `+values` `-max_value`, `-min_value` - - `SignalTargeting7`: `+max_value`, `+min_value`, `+signal_id`, `+signal_ref`, `+value_type` `-root` -- `bundled/property/create_property_list_request.py` - - **classes removed**: DataSubjectContestation7 -- `bundled/property/create_property_list_response.py` - - **classes removed**: DataSubjectContestation11 -- `bundled/property/delete_property_list_request.py` - - **classes removed**: DataSubjectContestation15 -- `bundled/property/get_property_list_request.py` - - **classes removed**: DataSubjectContestation39 -- `bundled/property/get_property_list_response.py` - - **classes removed**: DataSubjectContestation41 -- `bundled/property/list_property_lists_request.py` - - **classes removed**: DataSubjectContestation55 -- `bundled/property/list_property_lists_response.py` - - **classes added**: PricingOption161, PricingOption162, PricingOption163, PricingOption164, PricingOption165, PricingOption166 - - **classes removed**: DataSubjectContestation57, PricingOption211, PricingOption212, PricingOption213, PricingOption214, PricingOption215, PricingOption216 -- `bundled/property/update_property_list_request.py` - - **classes removed**: DataSubjectContestation139 -- `bundled/property/update_property_list_response.py` - - **classes added**: PricingOption211, PricingOption212, PricingOption213, PricingOption214, PricingOption215, PricingOption216 - - **classes removed**: DataSubjectContestation143, PricingOption341, PricingOption342, PricingOption343, PricingOption344, PricingOption345, PricingOption346 -- `bundled/property/validate_property_delivery_request.py` - - **classes removed**: DataSubjectContestation151 -- `bundled/signals/activate_signal_request.py` - - **classes removed**: DataSubjectContestation1 -- `bundled/signals/get_signals_request.py` - - **classes removed**: DataSubjectContestation45 -- `bundled/signals/get_signals_response.py` - - **classes added**: Signal - - **classes removed**: Deployments10, Deployments11, Deployments12, Deployments13, Deployments14, Deployments15, Deployments16, Deployments17, Deployments3, Deployments4, Deployments5, Deployments6, Deployments7, Deployments8, Deployments9, PricingOption14, PricingOption15, PricingOption16, PricingOption17, PricingOption18, Range9, Signals, Signals51, Signals52, Signals53, Signals54, Signals55, Signals6, Signals61, Signals62, Signals63, Signals64, Signals65 -- `bundled/sponsored_intelligence/si_send_message_response.py` - - **classes added**: Type26 - - **classes removed**: Type28 -- `core/audience_selector.py` - - **classes removed**: AudienceSelector5, AudienceSelector6, AudienceSelector7 - - `AudienceSelector2`: `+values` `-value` - - `AudienceSelector3`: `+max_value`, `+min_value` `-values` - - `AudienceSelector4`: `+category`, `+description` `-signal_id`, `-signal_ref`, `-value_type`, `-values` -- `core/brand_ref.py` - - **classes removed**: DataSubjectContestation5 -- `core/optimization_goal.py` - - **classes removed**: TargetFrequency1 -- `core/package_signal_targeting.py` - - `PackageSignalTargeting4`: `+activation_key`, `+pricing_option_id`, `+signal_agent_segment_id` `-max_value`, `-min_value`, `-signal_ref`, `-value_type` -- `core/product_filters.py` - - **classes added**: GeoProximityItem, RequiredVendorMetric, SignalTargetingItem7 - - **classes removed**: BudgetRange1, GeoProximity, GeoProximity3, GeoProximity4, RequiredVendorMetrics, RequiredVendorMetrics1 - - `SignalTargetingItem`: `+root` `-targeting_mode` - - `SignalTargetingItem2`: `+values` `-value` - - `SignalTargetingItem3`: `+max_value`, `+min_value` `-values` - - `SignalTargetingItem4`: `+targeting_mode` `-signal_id`, `-signal_ref`, `-value_type`, `-values` - - `SignalTargetingItem5`: `+targeting_mode` `-max_value`, `-min_value`, `-signal_id`, `-signal_ref`, `-value_type` - - `SignalTargetingItem6`: `+targeting_mode` `-max_value`, `-min_value`, `-signal_id`, `-signal_ref`, `-value_type` -- `core/signal_id.py` - - **classes added**: SignalId4, SignalId5 - - **classes removed**: SignalId26, SignalId27 -- `core/signal_ref.py` - - **classes added**: SignalRef4, SignalRef5, SignalRef6 - - **classes removed**: SignalRef37, SignalRef38, SignalRef39 -- `core/signal_targeting.py` - - **classes removed**: SignalTargeting4, SignalTargeting5, SignalTargeting6 - - `SignalTargeting2`: `+values` `-value` - - `SignalTargeting3`: `+max_value`, `+min_value` `-values` -- `core/signal_targeting_expression.py` - - **classes removed**: SignalTargetingExpression4 -- `core/targeting.py` - - **classes added**: GeoProximityItem - - **classes removed**: GeoProximity, GeoProximity6, GeoProximity7 -- `pricing_options/price_breakdown.py` - - **classes added**: Adjustment - - **classes removed**: Adjustments, Adjustments1 -- `signals/get_signals_response.py` - - **classes added**: Signal - - **classes removed**: Signals, Signals4 +_No field-shape changes detected._ diff --git a/schemas/cache/3.1.0-beta.5/a2ui/bound-value.json b/schemas/cache/3.1.0-beta.5/a2ui/bound-value.json new file mode 100644 index 000000000..94490eaad --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/a2ui/bound-value.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "A2UI Bound Value", + "description": "A value that can be a literal or bound to a path in the data model", + "oneOf": [ + { + "type": "object", + "description": "Literal string value", + "properties": { + "literalString": { + "type": "string", + "description": "Static string value" + } + }, + "required": [ + "literalString" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Literal number value", + "properties": { + "literalNumber": { + "type": "number", + "description": "Static number value" + } + }, + "required": [ + "literalNumber" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Literal boolean value", + "properties": { + "literalBoolean": { + "type": "boolean", + "description": "Static boolean value" + } + }, + "required": [ + "literalBoolean" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Path to data model value", + "properties": { + "path": { + "type": "string", + "description": "JSON pointer path to value in data model (e.g., '/products/0/title')" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "type": "object", + "description": "Literal with path binding (sets default and binds)", + "properties": { + "literalString": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "literalString", + "path" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/a2ui/component.json b/schemas/cache/3.1.0-beta.5/a2ui/component.json new file mode 100644 index 000000000..6ddc3b14f --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/a2ui/component.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "A2UI Component", + "description": "A component in an A2UI surface", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for this component within the surface" + }, + "parentId": { + "type": "string", + "description": "ID of the parent component (null for root)" + }, + "component": { + "type": "object", + "description": "Component definition (keyed by component type)", + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": { + "type": "object", + "description": "Component properties" + } + } + }, + "required": [ + "id", + "component" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/a2ui/si-catalog.json b/schemas/cache/3.1.0-beta.5/a2ui/si-catalog.json new file mode 100644 index 000000000..9596f806a --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/a2ui/si-catalog.json @@ -0,0 +1,426 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SI Component Catalog", + "description": "A2UI component catalog for Sponsored Intelligence", + "definitions": { + "Text": { + "type": "object", + "description": "Text display component", + "properties": { + "text": { + "$ref": "bound-value.json", + "description": "Text content to display" + }, + "variant": { + "type": "string", + "enum": [ + "body", + "heading", + "caption", + "label" + ], + "default": "body" + } + }, + "required": [ + "text" + ] + }, + "Button": { + "type": "object", + "description": "Interactive button component", + "properties": { + "label": { + "$ref": "bound-value.json", + "description": "Button label text" + }, + "action": { + "type": "object", + "description": "Action to trigger on click", + "properties": { + "name": { + "type": "string", + "description": "Action identifier" + }, + "context": { + "type": "object", + "description": "Context data to include with action", + "additionalProperties": { + "$ref": "bound-value.json" + } + } + }, + "required": [ + "name" + ] + }, + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary", + "text" + ], + "default": "primary" + }, + "disabled": { + "$ref": "bound-value.json" + } + }, + "required": [ + "label", + "action" + ] + }, + "Link": { + "type": "object", + "description": "Hyperlink component", + "properties": { + "label": { + "$ref": "bound-value.json", + "description": "Link text" + }, + "url": { + "$ref": "bound-value.json", + "description": "URL to navigate to" + }, + "external": { + "type": "boolean", + "description": "Opens in new tab if true", + "default": true + } + }, + "required": [ + "label", + "url" + ] + }, + "Image": { + "type": "object", + "description": "Image display component", + "properties": { + "src": { + "$ref": "bound-value.json", + "description": "Image source URL" + }, + "alt": { + "$ref": "bound-value.json", + "description": "Alt text for accessibility" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + } + }, + "required": [ + "src", + "alt" + ] + }, + "Card": { + "type": "object", + "description": "Card container for grouped content", + "properties": { + "title": { + "$ref": "bound-value.json", + "description": "Card title" + }, + "subtitle": { + "$ref": "bound-value.json", + "description": "Card subtitle" + }, + "image": { + "$ref": "bound-value.json", + "description": "Card image URL" + }, + "badge": { + "$ref": "bound-value.json", + "description": "Badge text (e.g., 'New', 'Sale')" + }, + "action": { + "type": "object", + "description": "Action to trigger on card click", + "properties": { + "name": { + "type": "string" + }, + "context": { + "type": "object", + "additionalProperties": { + "$ref": "bound-value.json" + } + } + }, + "required": [ + "name" + ] + }, + "children": { + "type": "array", + "description": "Child component IDs", + "items": { + "type": "string" + } + } + }, + "required": [ + "title" + ] + }, + "ProductCard": { + "type": "object", + "description": "Product display card (SI-specific)", + "properties": { + "title": { + "$ref": "bound-value.json", + "description": "Product name" + }, + "price": { + "$ref": "bound-value.json", + "description": "Price display string" + }, + "image": { + "$ref": "bound-value.json", + "description": "Product image URL" + }, + "description": { + "$ref": "bound-value.json", + "description": "Product description" + }, + "badge": { + "$ref": "bound-value.json", + "description": "Badge text (e.g., 'Best Seller')" + }, + "ctaLabel": { + "$ref": "bound-value.json", + "description": "CTA button label" + }, + "action": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "context": { + "type": "object", + "additionalProperties": { + "$ref": "bound-value.json" + } + } + }, + "required": [ + "name" + ] + } + }, + "required": [ + "title", + "price" + ] + }, + "List": { + "type": "object", + "description": "Data-bound list component", + "properties": { + "items": { + "$ref": "bound-value.json", + "description": "Path to array in data model" + }, + "template": { + "type": "object", + "description": "Template for list items", + "properties": { + "componentId": { + "type": "string", + "description": "ID of component to use as template" + } + }, + "required": [ + "componentId" + ] + }, + "emptyMessage": { + "$ref": "bound-value.json", + "description": "Message to show when list is empty" + }, + "layout": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "grid" + ], + "default": "vertical" + } + }, + "required": [ + "items", + "template" + ] + }, + "Row": { + "type": "object", + "description": "Horizontal layout container", + "properties": { + "children": { + "type": "array", + "description": "Child component IDs", + "items": { + "type": "string" + } + }, + "gap": { + "type": "string", + "description": "Gap between children", + "default": "8px" + }, + "align": { + "type": "string", + "enum": [ + "start", + "center", + "end", + "stretch" + ], + "default": "center" + }, + "justify": { + "type": "string", + "enum": [ + "start", + "center", + "end", + "between", + "around" + ], + "default": "start" + } + }, + "required": [ + "children" + ] + }, + "Column": { + "type": "object", + "description": "Vertical layout container", + "properties": { + "children": { + "type": "array", + "description": "Child component IDs", + "items": { + "type": "string" + } + }, + "gap": { + "type": "string", + "description": "Gap between children", + "default": "8px" + }, + "align": { + "type": "string", + "enum": [ + "start", + "center", + "end", + "stretch" + ], + "default": "stretch" + } + }, + "required": [ + "children" + ] + }, + "IntegrationAction": { + "type": "object", + "description": "MCP/A2A integration action button (SI-specific)", + "properties": { + "type": { + "type": "string", + "enum": [ + "mcp", + "a2a" + ], + "description": "Integration type" + }, + "label": { + "$ref": "bound-value.json", + "description": "Button label" + }, + "url": { + "$ref": "bound-value.json", + "description": "Integration endpoint URL" + }, + "highlighted": { + "type": "boolean", + "description": "Visually emphasize this action", + "default": false + } + }, + "required": [ + "type", + "label" + ] + }, + "AppHandoff": { + "type": "object", + "description": "Platform app handoff (SI-specific)", + "properties": { + "apps": { + "type": "object", + "description": "Platform-specific app configurations", + "additionalProperties": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "deepLink": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + } + } + } + }, + "required": [ + "apps" + ] + } + }, + "type": "object", + "properties": { + "catalogId": { + "type": "string", + "const": "si-standard", + "description": "SI standard component catalog" + }, + "components": { + "type": "array", + "description": "Available component types", + "items": { + "type": "string", + "enum": [ + "Text", + "Button", + "Link", + "Image", + "Card", + "ProductCard", + "List", + "Row", + "Column", + "IntegrationAction", + "AppHandoff" + ] + } + } + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/a2ui/surface.json b/schemas/cache/3.1.0-beta.5/a2ui/surface.json new file mode 100644 index 000000000..40aba240d --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/a2ui/surface.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "A2UI Surface", + "description": "A contiguous UI region containing components", + "type": "object", + "properties": { + "surfaceId": { + "type": "string", + "description": "Unique identifier for this surface" + }, + "catalogId": { + "type": "string", + "description": "Component catalog to use for rendering", + "default": "standard" + }, + "components": { + "type": "array", + "description": "Flat list of components (adjacency list structure)", + "items": { + "$ref": "component.json" + } + }, + "rootId": { + "type": "string", + "description": "ID of the root component (if not specified, first component is root)" + }, + "dataModel": { + "type": "object", + "description": "Application data that components can bind to", + "additionalProperties": true + } + }, + "required": [ + "surfaceId", + "components" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/a2ui/user-action.json b/schemas/cache/3.1.0-beta.5/a2ui/user-action.json new file mode 100644 index 000000000..9c9a9ccc6 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/a2ui/user-action.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "A2UI User Action", + "description": "User interaction event sent from client to agent", + "type": "object", + "properties": { + "surfaceId": { + "type": "string", + "description": "ID of the surface where the action occurred" + }, + "componentId": { + "type": "string", + "description": "ID of the component that triggered the action" + }, + "action": { + "type": "object", + "description": "The action that was triggered", + "properties": { + "name": { + "type": "string", + "description": "Action identifier (e.g., 'view_product', 'add_to_cart')" + }, + "context": { + "type": "object", + "description": "Context data resolved from data model bindings", + "additionalProperties": true + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the action occurred" + } + }, + "required": [ + "surfaceId", + "componentId", + "action" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/aao/agent-publishers.json b/schemas/cache/3.1.0-beta.5/aao/agent-publishers.json new file mode 100644 index 000000000..ab1123809 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/aao/agent-publishers.json @@ -0,0 +1,139 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AAO Directory: Agent \u2192 Publishers Inverse Lookup Response", + "description": "Response envelope for `GET /v1/agents/{agent_url}/publishers` against an AAO-compatible directory. Returns the set of publishers whose adagents.json authorizes the given agent_url, with provenance (discovery_method, manager_domain), per-publisher property counts, and lifecycle status. The publisher's own adagents.json remains the trust root; this endpoint is discovery, not authorization. Each `publisher_domain` row tells the operator which publishers to verify directly via the SDK's per-domain primitives.", + "type": "object", + "required": [ + "agent_url", + "directory_indexed_at", + "publishers" + ], + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "Canonicalized echo of the agent_url that was looked up. Lowercase host, default port stripped, trailing slash on path component normalized \u2014 matches the canonicalization the SDK applies in `verify_agent_authorization`." + }, + "directory_indexed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When the directory last completed a refresh of any publisher in this result. Provenance anchor for the consumer's own cache. NULL on empty pages \u2014 there's no per-publisher anchor to report; consumers SHOULD treat a null value as 'no freshness assertion for this response' and not advance their own cache freshness." + }, + "publishers": { + "type": "array", + "description": "Publishers whose adagents.json authorizes this agent. Empty array is a valid response (the directory has indexed this agent but no current authorizations resolve).", + "items": { + "$ref": "#/definitions/PublisherEntry" + } + }, + "next_cursor": { + "type": [ + "string", + "null" + ], + "description": "Opaque pagination cursor. Absent or null on the terminal page. Stable across the directory's refresh cycle for the lifetime of the cursor." + } + }, + "additionalProperties": false, + "definitions": { + "PublisherEntry": { + "type": "object", + "required": [ + "publisher_domain", + "discovery_method", + "properties_authorized", + "properties_total", + "status", + "last_verified_at" + ], + "properties": { + "publisher_domain": { + "type": "string", + "description": "Publisher whose adagents.json authorizes the agent.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "discovery_method": { + "type": "string", + "description": "How the directory discovered this authorization. `direct`: agent listed in publisher's own /.well-known/adagents.json. `authoritative_location`: publisher's /.well-known/adagents.json declared `authoritative_location` pointing to a manager file that lists the agent. `adagents_authoritative`: discovered via the manager file (publisher-declared in the manager's own properties[]). `ads_txt_managerdomain`: discovered via the publisher's ads.txt `MANAGERDOMAIN=` directive pointing to the manager file. The last three paths converge on the same manager file but have different trust profiles \u2014 the `managerdomain` path is the weakest because the manager file's `publisher_domain` anchor (the `managerdomain` fallback safety rule) is the only positive cross-check.", + "enum": [ + "direct", + "authoritative_location", + "adagents_authoritative", + "ads_txt_managerdomain" + ] + }, + "manager_domain": { + "type": [ + "string", + "null" + ], + "description": "Domain of the manager file that authorizes this publisher \u2192 agent edge. Required when `discovery_method` is `authoritative_location`, `adagents_authoritative`, or `ads_txt_managerdomain`. Null or absent when `discovery_method` is `direct`.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "properties_authorized": { + "type": "integer", + "minimum": 0, + "description": "Count of properties under THIS `publisher_domain` only that the agent's selectors resolve to. Never a network-wide count. The directory computes this by applying the publisher's `adagents.json` selector predicates against the publisher's own properties (federated) OR against the parent file's inline properties carrying matching `publisher_domain` (inline, per adcp#4825 resolution rule)." + }, + "properties_total": { + "type": "integer", + "minimum": 0, + "description": "Count of properties under THIS `publisher_domain` only \u2014 total inventory the publisher's file declares. Never a network-wide count. On managed-network-shape parent files (per adcp#4825 inline resolution), this is the count of inline `properties[]` entries whose `publisher_domain` field matches this row's domain." + }, + "property_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Canonical list of `property_id`s under THIS `publisher_domain` that the agent's selectors resolve to. Present iff the request included `?include=properties`; absent otherwise. The set is the same population `properties_authorized` counts, surfaced as IDs so consumers can run full set-diff against a federated fetch (count-equality is not set-equality \u2014 a publisher rotating N properties leaves the count unchanged but the set entirely different). Per-publisher scope; never network-wide. Order is unspecified; consumers should treat as a set." + }, + "signing_keys_pinned": { + "type": "boolean", + "description": "Whether the publisher's adagents.json entry for this agent pins `signing_keys[]`. When true, the agent's signed responses MUST verify against the pinned key set regardless of the agent's own JWKS. Operators should treat true as a signal that their published JWKS must match the publisher's pin." + }, + "status": { + "type": "string", + "description": "Lifecycle state for this publisher \u2192 agent edge. v1: `authorized` (selector resolves to \u2265 1 property under this publisher) and `revoked` (this publisher_domain newly appears in a parent file's `revoked_publisher_domains[]`; emitted as a tombstone on the next sync after revocation lands, then dropped). Future states (`unbound`, `pending`) deferred \u2014 the directory does not have the crawler state to emit them honestly.", + "enum": [ + "authorized", + "revoked" + ] + }, + "last_verified_at": { + "type": "string", + "format": "date-time", + "description": "When the directory last fetched and validated this publisher's adagents.json. Distinct from the envelope's `directory_indexed_at` \u2014 `last_verified_at` is per-publisher freshness, the envelope value is the most recent refresh in the result set." + } + }, + "allOf": [ + { + "if": { + "properties": { + "discovery_method": { + "enum": [ + "authoritative_location", + "adagents_authoritative", + "ads_txt_managerdomain" + ] + } + } + }, + "then": { + "required": [ + "manager_domain" + ], + "properties": { + "manager_domain": { + "type": "string" + } + } + } + } + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/get-account-financials-request.json b/schemas/cache/3.1.0-beta.5/account/get-account-financials-request.json new file mode 100644 index 000000000..773227dcb --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/get-account-financials-request.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Account Financials Request", + "description": "Request financial status for an operator-billed account. Returns spend summary, credit/balance status, and invoice history. Only applicable when the seller declares account_financials capability.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "properties": { + "account": { + "$ref": "../core/account-ref.json", + "description": "Account to query financials for. Must be an operator-billed account." + }, + "period": { + "$ref": "../core/date-range.json", + "description": "Date range for the spend summary. Defaults to the current billing cycle if omitted." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "account" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Query by account ID for current billing cycle", + "data": { + "account": { + "account_id": "acc_acme_001" + } + } + }, + { + "description": "Query by natural key for a specific period", + "data": { + "account": { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com" + }, + "period": { + "start": "2026-01-01", + "end": "2026-01-31" + } + } + }, + { + "description": "Agency querying financials for a client account", + "data": { + "account": { + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com" + } + } + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/get-account-financials-response.json b/schemas/cache/3.1.0-beta.5/account/get-account-financials-response.json new file mode 100644 index 000000000..1f5654f56 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/get-account-financials-response.json @@ -0,0 +1,331 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Account Financials Response", + "description": "Financial status for an operator-billed account. Returns spend summary, credit/balance status, payment status, and invoice history. The level of detail varies by seller \u2014 only account, currency, and period are guaranteed on success.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "GetAccountFinancialsSuccess", + "description": "Financial data retrieved successfully", + "type": "object", + "properties": { + "account": { + "$ref": "../core/account-ref.json", + "description": "Account reference, echoed from the request" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code for all monetary amounts in this response" + }, + "period": { + "$ref": "../core/date-range.json", + "description": "The actual period covered by spend data. May differ from the requested period if the seller adjusts to billing cycle boundaries." + }, + "timezone": { + "type": "string", + "description": "IANA timezone of the seller's billing day boundaries (e.g., 'America/New_York'). All dates in this response \u2014 period, invoice periods, due dates \u2014 are calendar dates in this timezone. Buyers in a different timezone should expect spend boundaries to differ from their own calendar day." + }, + "spend": { + "type": "object", + "description": "Spend summary for the period", + "properties": { + "total_spend": { + "type": "number", + "minimum": 0, + "description": "Total spend in the period, in currency" + }, + "media_buy_count": { + "type": "integer", + "minimum": 0, + "description": "Number of active media buys in the period" + } + }, + "required": [ + "total_spend" + ] + }, + "credit": { + "type": "object", + "description": "Credit status. Present for credit-based accounts (payment_terms like net_30).", + "properties": { + "credit_limit": { + "type": "number", + "minimum": 0, + "description": "Maximum outstanding balance allowed" + }, + "available_credit": { + "type": "number", + "description": "Remaining credit available (credit_limit minus outstanding balance)" + }, + "utilization_percent": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Credit utilization as a percentage (0-100)" + } + }, + "required": [ + "credit_limit", + "available_credit" + ] + }, + "balance": { + "type": "object", + "description": "Prepay balance. Present for prepay accounts.", + "properties": { + "available": { + "type": "number", + "minimum": 0, + "description": "Remaining prepaid balance" + }, + "last_top_up": { + "type": "object", + "description": "Most recent balance top-up", + "properties": { + "amount": { + "type": "number", + "minimum": 0, + "description": "Top-up amount" + }, + "date": { + "type": "string", + "format": "date", + "description": "Date of top-up" + } + }, + "required": [ + "amount", + "date" + ] + } + }, + "required": [ + "available" + ] + }, + "payment_status": { + "type": "string", + "enum": [ + "current", + "past_due", + "suspended" + ], + "description": "Overall payment status. current: all obligations met. past_due: one or more invoices overdue. suspended: account suspended due to payment issues." + }, + "payment_terms": { + "$ref": "../enums/payment-terms.json", + "description": "Payment terms in effect for this account" + }, + "invoices": { + "type": "array", + "description": "Recent invoices. Sellers may limit the number returned.", + "items": { + "type": "object", + "properties": { + "invoice_id": { + "type": "string", + "description": "Seller-assigned invoice identifier" + }, + "period": { + "$ref": "../core/date-range.json", + "description": "Billing period covered by this invoice" + }, + "amount": { + "type": "number", + "minimum": 0, + "description": "Invoice total in currency" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "issued", + "paid", + "past_due", + "void" + ], + "description": "Invoice status" + }, + "due_date": { + "type": "string", + "format": "date", + "description": "Payment due date" + }, + "paid_date": { + "type": "string", + "format": "date", + "description": "Date payment was received. Present when status is 'paid'." + } + }, + "required": [ + "invoice_id", + "amount", + "status" + ] + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "account", + "currency", + "period", + "timezone" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "GetAccountFinancialsError", + "description": "Operation failed \u2014 financials not available", + "type": "object", + "properties": { + "errors": { + "type": "array", + "description": "Operation-level errors", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "currency" + ] + }, + { + "required": [ + "period" + ] + }, + { + "required": [ + "timezone" + ] + } + ] + } + } + ], + "examples": [ + { + "description": "Credit account \u2014 current, with invoice history", + "data": { + "status": "completed", + "account": { + "account_id": "acc_acme_001" + }, + "currency": "USD", + "period": { + "start": "2026-02-01", + "end": "2026-02-28" + }, + "timezone": "America/New_York", + "spend": { + "total_spend": 45230, + "media_buy_count": 3 + }, + "credit": { + "credit_limit": 100000, + "available_credit": 54770, + "utilization_percent": 45.23 + }, + "payment_status": "current", + "payment_terms": "net_30", + "invoices": [ + { + "invoice_id": "inv_2026_01", + "period": { + "start": "2026-01-01", + "end": "2026-01-31" + }, + "amount": 38500, + "status": "paid", + "due_date": "2026-02-28", + "paid_date": "2026-02-15" + } + ] + } + }, + { + "description": "Prepay account with low balance", + "data": { + "status": "completed", + "account": { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com" + }, + "currency": "USD", + "period": { + "start": "2026-02-01", + "end": "2026-02-28" + }, + "timezone": "America/Los_Angeles", + "spend": { + "total_spend": 8200, + "media_buy_count": 2 + }, + "balance": { + "available": 1800, + "last_top_up": { + "amount": 10000, + "date": "2026-02-01" + } + }, + "payment_status": "current", + "payment_terms": "prepay" + } + }, + { + "description": "Agent-billed account \u2014 not supported", + "data": { + "status": "completed", + "errors": [ + { + "code": "UNSUPPORTED_FEATURE", + "message": "Financial data is not available for agent-billed accounts. The agent's own billing system is the source of truth." + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/list-accounts-request.json b/schemas/cache/3.1.0-beta.5/account/list-accounts-request.json new file mode 100644 index 000000000..cc472dcdb --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/list-accounts-request.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "List Accounts Request", + "description": "Request parameters for listing accounts accessible to the authenticated agent", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "active", + "pending_approval", + "rejected", + "payment_required", + "suspended", + "closed" + ], + "description": "Filter accounts by status. Omit to return accounts in all statuses." + }, + "pagination": { + "$ref": "../core/pagination-request.json" + }, + "sandbox": { + "type": "boolean", + "description": "Filter by sandbox status. true returns only sandbox accounts, false returns only production accounts. Omit to return all accounts. Primarily used with explicit accounts (require_operator_auth: true) where sandbox accounts are pre-existing test accounts on the platform." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/list-accounts-response.json b/schemas/cache/3.1.0-beta.5/account/list-accounts-response.json new file mode 100644 index 000000000..8739bbbdf --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/list-accounts-response.json @@ -0,0 +1,130 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "List Accounts Response", + "description": "Response payload for list_accounts task", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "properties": { + "accounts": { + "type": "array", + "description": "Array of accounts accessible to the authenticated agent. Each entry is the full Account object plus an optional `authorization` object describing what the calling agent is permitted to do on that account.", + "items": { + "allOf": [ + { + "$ref": "../core/account.json" + }, + { + "type": "object", + "properties": { + "authorization": { + "$ref": "../core/account-authorization.json", + "description": "Optional. The caller's scope grant against this account. Vendor agents of any type (media-buy, signals, governance, creative, brand) that support scope introspection SHOULD populate this so callers can preempt SCOPE_INSUFFICIENT / FIELD_NOT_PERMITTED errors rather than discovering scope by trial and error. Media-buy sales agents claiming the `attestation_verifier` standard scope MUST populate it. Absence means the vendor agent does not advertise introspectable scope for this account \u2014 callers MUST NOT infer access from absence, and fall back to error-driven discovery via the RBAC error codes." + } + } + } + ] + } + }, + "errors": { + "type": "array", + "description": "Task-specific errors and warnings", + "items": { + "$ref": "../core/error.json" + } + }, + "pagination": { + "$ref": "../core/pagination-response.json" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "accounts" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Agency with multiple client accounts", + "data": { + "status": "completed", + "accounts": [ + { + "account_id": "acc_acme_pinnacle", + "name": "Acme c/o Pinnacle", + "advertiser": "Acme Corp", + "billing_proxy": "Pinnacle Media", + "brand": { + "domain": "acme-corp.com" + }, + "operator": "pinnacle-media.com", + "billing": "operator", + "account_scope": "operator_brand", + "status": "active" + }, + { + "account_id": "acc_nova_pinnacle", + "name": "Nova c/o Pinnacle", + "advertiser": "Nova Brands", + "billing_proxy": "Pinnacle Media", + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com", + "billing": "operator", + "account_scope": "operator_brand", + "status": "active" + }, + { + "account_id": "acc_pinnacle", + "name": "Pinnacle", + "advertiser": "Pinnacle Media", + "brand": { + "domain": "pinnacle-media.com" + }, + "operator": "pinnacle-media.com", + "billing": "operator", + "account_scope": "operator", + "status": "active" + } + ], + "pagination": { + "has_more": false, + "total_count": 3 + } + } + }, + { + "description": "Direct advertiser with single account", + "data": { + "status": "completed", + "accounts": [ + { + "account_id": "acc_acme_direct", + "name": "Acme", + "advertiser": "Acme Corp", + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "billing": "operator", + "account_scope": "brand", + "status": "active", + "rate_card": "acme_vip_2024" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/report-usage-request.json b/schemas/cache/3.1.0-beta.5/account/report-usage-request.json new file mode 100644 index 000000000..5154b14c5 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/report-usage-request.json @@ -0,0 +1,188 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Report Usage Request", + "description": "Reports how a vendor's service was consumed after campaign delivery. Used by orchestrators (DSPs, storefronts) to inform vendor agents (signals, governance, creative, or \u2014 when the receiving agent is the seller of the media buy itself \u2014 a sales agent for buyer-attested or vendor-attested billing reconciliation) what was used so the receiver can track earned revenue and verify billing. Records can span multiple accounts and campaigns in a single request. When the buy's `measurement_terms.billing_measurement.vendor` names a party other than the seller's own ad server, that party (or the buyer on its behalf) pushes final measurements via this task; the seller invoices against those final records. See [Billing authority](/docs/media-buy/advanced-topics/billing-authority) for the end-to-end flow.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-mutates-state": true, + "properties": { + "idempotency_key": { + "type": "string", + "description": "Client-generated unique key for this request. If a request with the same key has already been accepted, the server returns the original response without re-processing. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request. Prevents duplicate billing on retries." + }, + "reporting_period": { + "$ref": "../core/datetime-range.json", + "description": "The time range covered by this usage report. Applies to all records in the request." + }, + "usage": { + "type": "array", + "description": "One or more usage records. Each record is self-contained: it carries its own account, allowing a single request to span multiple accounts.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "account": { + "$ref": "../core/account-ref.json", + "description": "Account for this usage record." + }, + "media_buy_id": { + "type": "string", + "description": "Seller-assigned media buy identifier. Links this usage record to a specific media buy.", + "x-entity": "media_buy" + }, + "vendor_cost": { + "type": "number", + "minimum": 0, + "description": "Amount owed to the vendor for this record, denominated in currency." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code." + }, + "pricing_option_id": { + "type": "string", + "description": "Pricing option identifier from the vendor's discovery response (e.g., get_signals, list_content_standards). The vendor uses this to verify the correct rate was applied.", + "x-entity": "vendor_pricing_option" + }, + "impressions": { + "type": "integer", + "minimum": 0, + "description": "Impressions delivered using this vendor service." + }, + "media_spend": { + "type": "number", + "minimum": 0, + "description": "Media spend in currency for the period. Required when a percent_of_media pricing model was used, so the vendor can verify the applied rate." + }, + "signal_agent_segment_id": { + "type": "string", + "description": "Signal identifier from get_signals. Required for signals agents.", + "x-entity": "signal_activation_id" + }, + "standards_id": { + "type": "string", + "description": "Content standards configuration identifier. Required for governance agents.", + "x-entity": "content_standards" + }, + "rights_id": { + "type": "string", + "description": "Rights grant identifier from acquire_rights. Required for brand/rights agents. Links usage records to specific rights grants for cap tracking, billing verification, and overage calculation.", + "x-entity": "rights_grant" + }, + "creative_id": { + "type": "string", + "description": "Creative identifier from build_creative or list_creatives. Required for creative agents. Links usage records to specific creatives for billing verification.", + "x-entity": "creative" + }, + "property_list_id": { + "type": "string", + "description": "Property list identifier from list_property_lists. Required for property list agents. Links usage records to specific property lists for billing verification.", + "x-entity": "property_list" + }, + "final": { + "type": "boolean", + "description": "Whether this usage record represents the reporter's final, billing-authoritative numbers for the reporting period. **Absent means unknown** \u2014 the reporter has not declared finality on this record. Set `true` only when the reporter has actually settled the numbers (e.g., 3PAS month-end close after SIVT scrubbing, conversion dedup, and view-through windows have closed; vendor file post-C7 for broadcast). Set `false` when pushing preliminary measurements (daily pacing pushes, intra-period progress) that are still settling. Receivers MUST NOT invoice on `final: false` records, and MUST NOT invoice on records where `final` is absent for buys whose `measurement_terms.billing_measurement` names this reporter as authoritative \u2014 request a final record first. Receivers MAY invoice on absent for buys with no `measurement_terms.billing_measurement` (3.0-style usage where the receiver treats reports as authoritative on receipt) and for non-media-buy variants (signals, governance, creative, brand \u2014 domains with no provisional state concept). When the same `(account, media_buy_id, reporting_period)` is later reported with `final: true`, that record supersedes any prior records for the period." + }, + "finalized_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp at which the reporter considered these numbers final. Present only when `final: true`. Anchors any deadline declared in the buy's `measurement_terms.billing_measurement.finalization_deadline_hours`." + }, + "measurement_window": { + "type": "string", + "maxLength": 50, + "description": "Which measurement window this record represents, referencing a window_id from the product's reporting_capabilities.measurement_windows or from `measurement_terms.billing_measurement.measurement_window`. Examples: 'c7' for broadcast TV, 'post_sivt' for digital post-IVT, 'downloads_30d' for podcast. When absent, the record is not windowed (standard digital reporting). When the buy's `measurement_terms.billing_measurement.measurement_window` is set, reporters SHOULD include `measurement_window` so the receiver can reconcile against the correct stage.", + "examples": [ + "live", + "c3", + "c7", + "post_ivt", + "post_sivt", + "downloads_30d" + ] + } + }, + "required": [ + "account", + "vendor_cost", + "currency" + ], + "additionalProperties": true + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "reporting_period", + "usage" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Signal usage for a single campaign", + "data": { + "idempotency_key": "550e8400-e29b-41d4-a716-446655440000", + "reporting_period": { + "start": "2025-03-01T00:00:00Z", + "end": "2025-03-31T23:59:59Z" + }, + "usage": [ + { + "account": { + "account_id": "acct_pinnacle_signals" + }, + "signal_agent_segment_id": "luxury_auto_intenders", + "pricing_option_id": "po_lux_auto_cpm", + "impressions": 4200000, + "media_spend": 21000, + "vendor_cost": 2100, + "currency": "USD" + } + ] + } + }, + { + "description": "Multi-account batch across two campaigns", + "data": { + "idempotency_key": "8b7a9c2d-3456-4789-abcd-ef0123456789", + "reporting_period": { + "start": "2025-03-01T00:00:00Z", + "end": "2025-03-31T23:59:59Z" + }, + "usage": [ + { + "account": { + "account_id": "acct_pinnacle_signals" + }, + "signal_agent_segment_id": "luxury_auto_intenders", + "pricing_option_id": "po_lux_auto_cpm", + "impressions": 2100000, + "vendor_cost": 1050, + "currency": "USD" + }, + { + "account": { + "account_id": "acct_nova" + }, + "signal_agent_segment_id": "eco_conscious_shoppers", + "pricing_option_id": "po_eco_cpm", + "impressions": 800000, + "vendor_cost": 400, + "currency": "USD" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/report-usage-response.json b/schemas/cache/3.1.0-beta.5/account/report-usage-response.json new file mode 100644 index 000000000..1adfc1532 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/report-usage-response.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Report Usage Response", + "description": "Response from report_usage. Partial acceptance is valid \u2014 records that pass validation are stored even when others fail.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "properties": { + "accepted": { + "type": "integer", + "minimum": 0, + "description": "Number of usage records successfully stored." + }, + "errors": { + "type": "array", + "description": "Validation errors for individual records. The field property identifies which record failed (e.g., 'usage[1].pricing_option_id').", + "items": { + "$ref": "../core/error.json" + } + }, + "sandbox": { + "type": "boolean", + "description": "When true, the account is a sandbox account and no billing occurred." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "accepted" + ], + "additionalProperties": true, + "examples": [ + { + "description": "All records accepted", + "data": { + "status": "completed", + "accepted": 2 + } + }, + { + "description": "Partial acceptance \u2014 one record failed validation", + "data": { + "status": "completed", + "accepted": 1, + "errors": [ + { + "code": "INVALID_PRICING_OPTION", + "message": "pricing_option_id 'po_unknown' does not exist on this account", + "field": "usage[1].pricing_option_id" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/sync-accounts-request.json b/schemas/cache/3.1.0-beta.5/account/sync-accounts-request.json new file mode 100644 index 000000000..029652118 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/sync-accounts-request.json @@ -0,0 +1,342 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Sync Accounts Request", + "description": "Sync advertiser account state with a seller. Two modes, distinguished by the key on each per-account entry:\n\n- **Provisioning mode** (`brand` + `operator` + `billing` at the entry root): the agent declares which brands it represents, who operates on each brand's behalf, and the billing model. The seller provisions or links accounts via upsert. Used for implicit accounts (`require_operator_auth: false`).\n\n- **Settings-update mode** (`account` field carrying an [`AccountRef`](/schemas/core/account-ref.json)): targets an existing account by `account_id` (or by natural key for the implicit case). The seller updates the account's settable state (notification subscriptions, payment terms, billing entity refinements) \u2014 no provisioning side effects. Used for explicit accounts (`require_operator_auth: true`) where accounts are pre-provisioned out of band, and discovered via `list_accounts`. Implicit-account sellers MAY also accept this mode for settings updates against accounts they previously provisioned.\n\nExactly one of the two key shapes is allowed per entry. Sellers that do not implement settings-update mode MUST return `UNSUPPORTED_PROVISIONING` on entries keyed by `account.account_id`; sellers that do not provision MUST return `UNSUPPORTED_PROVISIONING` on entries keyed by the natural-key trio.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-mutates-state": true, + "properties": { + "idempotency_key": { + "type": "string", + "description": "Client-generated unique key for at-most-once execution. Natural per-account upsert keys (brand, operator) handle resource-level dedup, but the envelope triggers onboarding webhooks, billing setup, and audit events \u2014 this key prevents those side effects from firing twice on retry. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "accounts": { + "type": "array", + "description": "Per-account sync entries. Each entry uses one of two key shapes: the `account` field (AccountRef) for settings-update mode, or the flat `brand` + `operator` + `billing` trio for provisioning mode.", + "items": { + "type": "object", + "description": "An advertiser account entry \u2014 either provisions/upserts a new account (natural key) or updates an existing one (AccountRef key).", + "properties": { + "account": { + "$ref": "../core/account-ref.json", + "description": "Settings-update key. When present, this entry targets an existing account by `account_id` (explicit) or natural key (implicit, settings-update against a previously-provisioned account). Mutually exclusive with the flat `brand` + `operator` + `billing` provisioning trio. When `account` is present, the seller MUST NOT create a new account \u2014 entries that would otherwise trigger provisioning are rejected with `UNSUPPORTED_PROVISIONING`." + }, + "brand": { + "$ref": "../core/brand-ref.json", + "description": "Brand reference identifying the advertiser. Required for **provisioning mode**; MUST be absent in settings-update mode." + }, + "operator": { + "type": "string", + "description": "Domain of the entity operating on the brand's behalf (e.g., 'pinnacle-media.com'). When the brand operates directly, this is the brand's domain. Verified against the brand's authorized_operators in brand.json. Required for **provisioning mode**; MUST be absent in settings-update mode.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "billing": { + "$ref": "../enums/billing-party.json", + "description": "Who should be invoiced. Required for **provisioning mode**; MUST be absent in settings-update mode (billing is fixed at provisioning time and cannot be changed via settings-update)." + }, + "billing_entity": { + "$ref": "../core/business-entity.json", + "description": "Business entity details for the party responsible for payment. The agent provides this so the seller has the legal name, tax IDs, address, and bank details needed for formal B2B invoicing. Permitted in both modes \u2014 sellers MAY accept refinements in settings-update mode (e.g., updated bank details)." + }, + "payment_terms": { + "$ref": "../enums/payment-terms.json", + "description": "Payment terms for this account. The seller must either accept these terms or reject the account \u2014 terms are never silently remapped. When omitted, the seller applies its default terms. Permitted in both modes." + }, + "sandbox": { + "type": "boolean", + "description": "When true, provision this as a sandbox account with no real platform calls or billing. Only applicable to implicit accounts (require_operator_auth: false) in provisioning mode. For explicit accounts, sandbox accounts are pre-existing test accounts discovered via list_accounts." + }, + "preferred_reporting_protocol": { + "$ref": "../enums/cloud-storage-protocol.json", + "description": "Buyer's preferred cloud storage protocol for offline reporting delivery. The seller provisions the account's reporting_bucket using this protocol if supported. When omitted, the seller chooses from its supported offline_delivery_protocols. Only meaningful when the seller's reporting_delivery_methods includes 'offline'." + }, + "notification_configs": { + "type": "array", + "description": "Account-level webhook subscriptions for notifications whose lifecycle outlives any single media buy (`creative.status_changed`, `creative.purged`, wholesale feed change payloads, future account-anchored resource events after those event types are added to `notification-type.json`). This surface does not currently carry lifecycle events for the account object itself (for example, there is no `account.status_changed` event type); account status changes are observed through `list_accounts` polling or the one-shot `sync_accounts.push_notification_config` async result channel. Declarative replace semantics: when this field is present, the buyer sends the full desired array and the seller replaces the account's current set with that array, keyed by account-scoped `subscriber_id`. Omit this field to leave existing subscribers unchanged; send `[]` to remove all subscribers. Re-sending an existing `subscriber_id` for the account replaces that subscriber's config rather than creating a duplicate; persisted entries whose `subscriber_id` does not appear in the sent array are removed, so the seller MUST NOT merge the new array with persisted state. Paused entries (`active: false`) use the same replacement semantics; a buyer that wants to preserve a paused subscriber MUST re-include it with `active: false`. Duplicate `subscriber_id` values within one submitted array are rejected. Permitted in both provisioning and settings-update modes. Each entry registers a URL, the event types the subscriber wants, and optional legacy auth \u2014 see [`notification-config.json`](/schemas/core/notification-config.json). The seller MUST echo applied state on the response and on `list_accounts` reads, with `authentication.credentials` omitted (write-only). Sellers MUST reject entries whose `event_types` include any type whose contract anchors at a media buy or below (today: `scheduled`, `final`, `delayed`, `adjusted`, `impairment`) or account-lifecycle names not present in the enum as per-account validation failures with `INVALID_REQUEST` or `VALIDATION_ERROR` and `error.field` pointing at the invalid `event_types` entry \u2014 those events do not belong on this surface. Wholesale feed webhook registrations carry the actual change payload in `/schemas/core/wholesale-feed-webhook.json`; receivers use `get_products` / `get_signals` with `if_wholesale_feed_version` to repair or reconcile. This is distinct from sync_catalogs, which manages buyer-provided campaign input feeds on a seller account.\n\nActivation proof: before activating a new or changed active subscriber, the seller MUST validate the URL, complete the account-level webhook proof-of-control challenge, and only then persist or expose the subscriber as `active: true`. A valid existing proof for the same `(account_id, subscriber_id, normalized url, authentication mode/credential binding, normalized event_types)` tuple MAY be reused; changing any element of that tuple requires fresh proof. The challenge POST itself MUST be signed with the seller's RFC 9421 webhook-signing key and MUST include seller_agent_url, delivery_auth, and event_types so the receiver can verify the pending registration before echoing the challenge. Entries sent with `active: false` may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time, and those entries MUST NOT receive fires until reactivated. If proof fails or times out, the seller rejects the account entry with `action: \"failed\"`, leaves the prior notification_configs[] set unchanged, and reports `VALIDATION_ERROR` (or `INVALID_REQUEST` for malformed URLs) at the failing `notification_configs[j].url` field.\n\n**Cap rationale:** `maxItems: 16` is a practical fan-out cap (governance + buyer ingestion + audit bus + dx team + a few partner hooks). The cap exists to prevent unbounded subscriber arrays in storage and to bound the seller's per-event fan-out work. Sellers that hit the cap with legitimate subscribers should surface this on the protocol roadmap rather than work around it.", + "items": { + "$ref": "../core/notification-config.json" + }, + "maxItems": 16 + } + }, + "oneOf": [ + { + "title": "ProvisioningMode", + "description": "Provisioning-mode entry \u2014 natural-key trio is required, `account` is forbidden.", + "required": [ + "brand", + "operator", + "billing" + ], + "not": { + "required": [ + "account" + ] + } + }, + { + "title": "SettingsUpdateMode", + "description": "Settings-update entry \u2014 `account` (AccountRef) is required, provisioning trio fields are forbidden.", + "required": [ + "account" + ], + "allOf": [ + { + "not": { + "required": [ + "brand" + ] + } + }, + { + "not": { + "required": [ + "operator" + ] + } + }, + { + "not": { + "required": [ + "billing" + ] + } + } + ] + } + ], + "additionalProperties": true + }, + "maxItems": 1000 + }, + "delete_missing": { + "type": "boolean", + "default": false, + "description": "When true, accounts previously synced by this agent but not included in this request will be deactivated. Scoped to the authenticated agent \u2014 does not affect accounts managed by other agents. Use with caution." + }, + "dry_run": { + "type": "boolean", + "default": false, + "description": "When true, preview what would change without applying. Returns what would be created/updated/deactivated." + }, + "push_notification_config": { + "$ref": "../core/push-notification-config.json", + "description": "Webhook for async notifications when account status changes (e.g., pending_approval transitions to active)." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "accounts" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Agency syncing multiple advertisers with different billing", + "data": { + "idempotency_key": "a7f9c2e4-1234-4567-89ab-cdef01234567", + "accounts": [ + { + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com", + "billing": "operator" + }, + { + "brand": { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + "operator": "pinnacle-media.com", + "billing": "agent" + } + ] + } + }, + { + "description": "Brand buying direct with payment terms", + "data": { + "idempotency_key": "b8e0d3f5-2345-4678-9abc-def012345678", + "accounts": [ + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "billing": "operator", + "payment_terms": "net_30" + } + ] + } + }, + { + "description": "Agent consolidating billing with net-60 terms", + "data": { + "idempotency_key": "c9f1e4a6-3456-4789-abcd-ef0123456789", + "accounts": [ + { + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com", + "billing": "agent", + "payment_terms": "net_60" + }, + { + "brand": { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + "operator": "pinnacle-media.com", + "billing": "agent", + "payment_terms": "net_60" + } + ] + } + }, + { + "description": "Advertiser billed directly with structured billing entity (DACH B2B)", + "data": { + "idempotency_key": "d0a2f5b7-4567-489a-bcde-f01234567890", + "accounts": [ + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "pinnacle-media.com", + "billing": "advertiser", + "billing_entity": { + "legal_name": "Acme Corporation GmbH", + "vat_id": "DE987654321", + "registration_number": "HRB 67890", + "address": { + "street": "Hauptstrasse 42", + "city": "Munich", + "postal_code": "80331", + "country": "DE" + }, + "contacts": [ + { + "role": "billing", + "name": "AP Department", + "email": "billing@acme-corp.com" + } + ], + "bank": { + "account_holder": "Acme Corporation GmbH", + "iban": "DE75512108001245126199", + "bic": "SOLADEST600" + } + }, + "payment_terms": "net_30" + } + ] + } + }, + { + "description": "Provisioning mode \u2014 register a creative-lifecycle webhook subscription alongside account provisioning", + "data": { + "idempotency_key": "e1b3a6c8-5678-49ab-cdef-1234567890ab", + "accounts": [ + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "billing": "operator", + "notification_configs": [ + { + "subscriber_id": "buyer-primary", + "url": "https://buyer.example/webhooks/adcp/creative", + "event_types": [ + "creative.status_changed", + "creative.purged" + ], + "active": true + } + ] + } + ] + } + }, + { + "description": "Settings-update mode \u2014 register webhook subscribers on an existing explicit account by account_id", + "data": { + "idempotency_key": "f2c4b7d9-6789-49bc-defa-2345678901bc", + "accounts": [ + { + "account": { + "account_id": "acc_acme_pinnacle" + }, + "notification_configs": [ + { + "subscriber_id": "buyer-primary", + "url": "https://buyer.example/webhooks/adcp/creative", + "event_types": [ + "creative.status_changed", + "creative.purged" + ], + "active": true + }, + { + "subscriber_id": "audit-bus", + "url": "https://audit.buyer.example/adcp/ingest", + "event_types": [ + "creative.status_changed", + "creative.purged" + ], + "active": true + } + ] + } + ] + } + }, + { + "description": "Settings-update mode \u2014 register a wholesale feed mirror webhook subscriber for wholesale product and signal changes", + "data": { + "idempotency_key": "a8af8cf1-89bd-41f3-b27d-7ee7e9f8d2e4", + "accounts": [ + { + "account": { + "account_id": "acc_acme_pinnacle" + }, + "notification_configs": [ + { + "subscriber_id": "wholesale-feed-sync", + "url": "https://buyer.example/webhooks/adcp/wholesale-feed", + "event_types": [ + "product.created", + "product.updated", + "product.priced", + "product.removed", + "signal.created", + "signal.updated", + "signal.priced", + "signal.removed", + "wholesale_feed.bulk_change" + ], + "active": true + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/sync-accounts-response.json b/schemas/cache/3.1.0-beta.5/account/sync-accounts-response.json new file mode 100644 index 000000000..5c24738d7 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/sync-accounts-response.json @@ -0,0 +1,371 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Sync Accounts Response", + "description": "Response from account sync operation. Returns per-account results with status and billing, or operation-level errors on complete failure.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "SyncAccountsSuccess", + "description": "Sync operation processed accounts (individual accounts may be pending or have action=failed)", + "type": "object", + "properties": { + "dry_run": { + "type": "boolean", + "description": "Whether this was a dry run (no actual changes made)" + }, + "accounts": { + "type": "array", + "description": "Results for each account processed", + "items": { + "type": "object", + "properties": { + "account_id": { + "type": "string", + "description": "Seller-assigned account identifier. Use this in subsequent create_media_buy and other account-scoped operations.", + "x-entity": "account" + }, + "brand": { + "$ref": "../core/brand-ref.json", + "description": "Brand reference, echoed from the request" + }, + "operator": { + "type": "string", + "description": "Operator domain, echoed from request" + }, + "name": { + "type": "string", + "description": "Human-readable account name assigned by the seller" + }, + "action": { + "type": "string", + "enum": [ + "created", + "updated", + "unchanged", + "failed" + ], + "description": "Action taken for this account. created: new account provisioned. updated: existing account modified. unchanged: no changes needed. failed: could not process (see errors)." + }, + "status": { + "type": "string", + "enum": [ + "active", + "pending_approval", + "rejected", + "payment_required", + "suspended", + "closed" + ], + "description": "Account status. active: ready for use. pending_approval: seller reviewing (credit, legal). rejected: seller declined the account request. payment_required: credit limit reached or funds depleted. suspended: was active, now paused. closed: was active, now terminated." + }, + "billing": { + "$ref": "../enums/billing-party.json", + "description": "Who is invoiced on this account. Matches the requested billing model." + }, + "billing_entity": { + "$ref": "../core/business-entity.json", + "description": "Business entity details for the party responsible for payment, echoed from the request. Sellers MAY add fields the agent omitted (e.g., filling in registration_number from a credit check), but MUST NOT return data from a different entity. Bank details are omitted (write-only)." + }, + "account_scope": { + "$ref": "../enums/account-scope.json" + }, + "setup": { + "type": "object", + "description": "Setup information for pending accounts. Provides the agent (or human) with next steps to complete account activation.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL where the human can complete the required action (credit application, legal agreement, add funds)" + }, + "message": { + "type": "string", + "description": "Human-readable description of what's needed" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When this setup link expires" + } + }, + "required": [ + "message" + ], + "additionalProperties": true + }, + "rate_card": { + "type": "string", + "description": "Rate card applied to this account" + }, + "payment_terms": { + "$ref": "../enums/payment-terms.json", + "description": "Payment terms agreed for this account. When the account is active, these are the binding terms for all invoices on this account." + }, + "credit_limit": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + } + }, + "required": [ + "amount", + "currency" + ] + }, + "errors": { + "type": "array", + "description": "Per-account errors (only present when action is 'failed')", + "items": { + "$ref": "../core/error.json" + } + }, + "warnings": { + "type": "array", + "description": "Non-fatal warnings about this account", + "items": { + "type": "string" + } + }, + "sandbox": { + "type": "boolean", + "description": "Whether this is a sandbox account, echoed from the request. Only present for implicit accounts." + }, + "notification_configs": { + "type": "array", + "description": "Applied notification subscribers for this account after declarative replacement and activation-proof checks. Present on `created`, `updated`, and `unchanged` results when the buyer included `notification_configs` in the request or any persisted entries exist on the account. Entries are keyed by account-scoped `subscriber_id`; re-sending an existing `subscriber_id` replaces that subscriber's config rather than creating a duplicate. Only configs that the seller has persisted are echoed. `authentication.credentials` is omitted on every entry (write-only).", + "items": { + "$ref": "../core/notification-config.json" + }, + "maxItems": 16 + }, + "authorization": { + "$ref": "../core/account-authorization.json", + "description": "Optional. The caller's scope grant against this account after the sync operation. Vendor agents of any type (media-buy, signals, governance, creative, brand) that support scope introspection SHOULD populate this so callers can preempt RBAC errors rather than discovering scope by trial and error. Media-buy sales agents claiming the `attestation_verifier` standard scope MUST populate it. Present on `created`, `updated`, and `unchanged` results; omitted on `failed` results (where the account did not reach a usable state). Absence means the vendor agent does not advertise introspectable scope \u2014 callers MUST NOT infer access from absence." + } + }, + "required": [ + "brand", + "operator", + "action", + "status" + ], + "additionalProperties": true + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "accounts" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "SyncAccountsError", + "description": "Operation failed completely, no accounts were processed", + "type": "object", + "properties": { + "errors": { + "type": "array", + "description": "Operation-level errors (e.g., authentication failure, service unavailable)", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "accounts" + ] + }, + { + "required": [ + "dry_run" + ] + } + ] + } + } + ], + "examples": [ + { + "description": "Mixed results \u2014 one active, one pending approval", + "data": { + "status": "completed", + "accounts": [ + { + "account_id": "acc_spark_001", + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com", + "name": "Spark (via Pinnacle)", + "action": "created", + "status": "active", + "billing": "operator", + "account_scope": "operator_brand" + }, + { + "account_id": "acc_glow_pending", + "brand": { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + "operator": "pinnacle-media.com", + "name": "Glow", + "action": "created", + "status": "pending_approval", + "billing": "operator", + "account_scope": "operator_brand", + "setup": { + "url": "https://seller.example.com/advertiser-onboard", + "message": "Complete advertiser registration and credit application", + "expires_at": "2026-03-10T00:00:00Z" + } + } + ] + } + }, + { + "description": "Rejected account \u2014 no account_id assigned", + "data": { + "status": "completed", + "accounts": [ + { + "brand": { + "domain": "acme-corp.com", + "brand_id": "clearance" + }, + "operator": "acme-corp.com", + "action": "created", + "status": "rejected", + "warnings": [ + "Account request declined: advertiser category not accepted on this platform." + ] + } + ] + } + }, + { + "description": "Unsupported billing \u2014 seller rejects the request", + "data": { + "status": "completed", + "accounts": [ + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "action": "failed", + "status": "rejected", + "errors": [ + { + "code": "BILLING_NOT_SUPPORTED", + "message": "Operator billing is not supported. This seller only accepts agent billing." + } + ] + } + ] + } + }, + { + "description": "Advertiser billed directly with billing entity (bank details omitted in response)", + "data": { + "status": "completed", + "accounts": [ + { + "account_id": "acc_acme_direct_bill", + "brand": { + "domain": "acme-corp.com" + }, + "operator": "pinnacle-media.com", + "name": "Acme Corp (billed direct)", + "action": "created", + "status": "active", + "billing": "advertiser", + "billing_entity": { + "legal_name": "Acme Corporation GmbH", + "vat_id": "DE987654321", + "registration_number": "HRB 67890", + "address": { + "street": "Hauptstrasse 42", + "city": "Munich", + "postal_code": "80331", + "country": "DE" + }, + "contacts": [ + { + "role": "billing", + "name": "AP Department", + "email": "billing@acme-corp.com" + } + ] + }, + "account_scope": "operator_brand", + "payment_terms": "net_30" + } + ] + } + }, + { + "description": "Unsupported payment terms \u2014 seller rejects the request", + "data": { + "status": "completed", + "accounts": [ + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "action": "failed", + "status": "rejected", + "errors": [ + { + "code": "PAYMENT_TERMS_NOT_SUPPORTED", + "message": "Net-60 payment terms are not available. Omit payment_terms to accept this seller's default terms (net_30)." + } + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/sync-governance-request.json b/schemas/cache/3.1.0-beta.5/account/sync-governance-request.json new file mode 100644 index 000000000..4c062fd5b --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/sync-governance-request.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Sync Governance Request", + "description": "Sync the governance agent endpoint against specific accounts. The seller persists the governance agent and calls it for approval during media buy lifecycle events via check_governance. Uses replace semantics: each call replaces any previously synced agent on the specified accounts. The seller MUST verify that the authenticated agent has authority over each referenced account before persisting the governance agent.\n\nThe binding is **account-scoped, not plan-scoped**. Each account binds to exactly one governance agent, and that agent owns the lifecycle for every plan on the account \u2014 `plan_id` is threaded through `check_governance` calls so the agent can route internally, but governance registration does not vary per plan. Senders MUST NOT attempt to bind different governance agents to different plans on the same account; the wire offers no field for it and brand/seller implementations resolve the bound agent from the account alone.\n\nA plan is unitary \u2014 budget authority, delivery monitoring, and regulatory compliance are phases of the same evaluation (`purchase` / `modification` / `delivery` on check_governance), not specialisms held by different agents \u2014 so a single agent owns the full lifecycle. Buyers that need internal specialist review (e.g., a separate legal reviewer) compose that inside the governance agent, not at the registration layer. `governance_agents` is an array (not a scalar) because that is the shape 3.0 shipped with and existing senders MUST continue to work; the `maxItems: 1` constraint is load-bearing and not anticipated to relax. The single-agent rule is also baked into the wire below this layer (`protocol-envelope.governance_context` is singular), so loosening `maxItems` here would require a coordinated change across the envelope and every task that threads the context.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-mutates-state": true, + "properties": { + "idempotency_key": { + "type": "string", + "description": "Client-generated unique key for at-most-once execution. `account` gives resource-level dedup, but governance changes emit audit events and can trigger reapproval flows \u2014 this key prevents those side effects from firing twice on retry. MUST be unique per (seller, request) pair. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "accounts": { + "type": "array", + "description": "Per-account governance agent configuration. Each entry pairs an account reference with the governance agents for that account.", + "items": { + "type": "object", + "properties": { + "account": { + "$ref": "../core/account-ref.json", + "description": "Account to sync governance agents for. Use account_id for explicit accounts or brand + operator for implicit accounts." + }, + "governance_agents": { + "type": "array", + "description": "Governance agent endpoint for this account. Exactly one entry \u2014 the single agent that owns the account's full governance lifecycle. The seller calls this agent via check_governance during media buy lifecycle events. The array shape is preserved for wire compatibility with 3.0 senders; `maxItems: 1` is load-bearing and mirrors the singular `governance_context` on the protocol envelope.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "Governance agent endpoint URL. Must use HTTPS." + }, + "authentication": { + "type": "object", + "description": "Authentication the seller presents when calling this governance agent.", + "properties": { + "schemes": { + "type": "array", + "items": { + "$ref": "../enums/auth-scheme.json" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Authentication credential (e.g., Bearer token).", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url", + "authentication" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1 + } + }, + "required": [ + "account", + "governance_agents" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 100 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "accounts" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Sync the governance agent on an explicit account (by account_id)", + "data": { + "idempotency_key": "e1b3a6c8-5678-489a-bcde-f01234567891", + "accounts": [ + { + "account": { + "account_id": "acct-social-001" + }, + "governance_agents": [ + { + "url": "https://governance.pinnacle-media.com", + "authentication": { + "schemes": [ + "Bearer" + ], + "credentials": "gov-token-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } + ] + } + ] + } + }, + { + "description": "Sync the governance agent on an implicit account (by brand + operator)", + "data": { + "idempotency_key": "f2c4b7d9-6789-489b-cdef-012345678902", + "accounts": [ + { + "account": { + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com" + }, + "governance_agents": [ + { + "url": "https://governance.pinnacle-media.com", + "authentication": { + "schemes": [ + "Bearer" + ], + "credentials": "gov-token-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" + } + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/account/sync-governance-response.json b/schemas/cache/3.1.0-beta.5/account/sync-governance-response.json new file mode 100644 index 000000000..484ae1a7c --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/account/sync-governance-response.json @@ -0,0 +1,186 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Sync Governance Response", + "description": "Response from governance agent sync. Returns per-account results confirming sync, or operation-level errors on complete failure.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "SyncGovernanceSuccess", + "description": "Sync processed \u2014 individual accounts may have errors", + "type": "object", + "properties": { + "accounts": { + "type": "array", + "description": "Per-account sync results", + "items": { + "type": "object", + "properties": { + "account": { + "$ref": "../core/account-ref.json", + "description": "Account reference, echoed from request" + }, + "status": { + "type": "string", + "enum": [ + "synced", + "failed" + ], + "description": "Sync result. synced: governance agents persisted. failed: could not complete (see errors)." + }, + "governance_agents": { + "type": "array", + "description": "Governance agent now synced on this account. Reflects the persisted state after sync. Exactly one entry; the array shape mirrors the request schema and the one-agent-per-account invariant. See sync_governance request schema.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "Governance agent endpoint URL." + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1 + }, + "errors": { + "type": "array", + "description": "Per-account errors (only present when status is 'failed')", + "items": { + "$ref": "../core/error.json" + } + } + }, + "required": [ + "account", + "status" + ], + "additionalProperties": true + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "accounts" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "SyncGovernanceError", + "description": "Operation failed completely, no accounts were processed", + "type": "object", + "properties": { + "errors": { + "type": "array", + "description": "Operation-level errors (e.g., authentication failure, service unavailable)", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "required": [ + "accounts" + ] + } + } + ], + "examples": [ + { + "description": "Governance agents synced on two accounts", + "data": { + "status": "completed", + "accounts": [ + { + "account": { + "account_id": "acct-social-001" + }, + "status": "synced", + "governance_agents": [ + { + "url": "https://governance.pinnacle-media.com" + } + ] + }, + { + "account": { + "account_id": "acct-social-002" + }, + "status": "synced", + "governance_agents": [ + { + "url": "https://governance.acme-buyer.com" + } + ] + } + ] + } + }, + { + "description": "Partial failure \u2014 one account not found", + "data": { + "status": "completed", + "accounts": [ + { + "account": { + "account_id": "acct-social-001" + }, + "status": "synced", + "governance_agents": [ + { + "url": "https://governance.pinnacle-media.com" + } + ] + }, + { + "account": { + "account_id": "acct-unknown" + }, + "status": "failed", + "errors": [ + { + "code": "ACCOUNT_NOT_FOUND", + "message": "Account 'acct-unknown' does not exist or is not accessible to the authenticated agent." + } + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/adagents.json b/schemas/cache/3.1.0-beta.5/adagents.json new file mode 100644 index 000000000..f62b883e7 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/adagents.json @@ -0,0 +1,1184 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdCP Agents Authorization", + "description": "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.", + "oneOf": [ + { + "type": "object", + "description": "URL reference variant - points to the authoritative location of the adagents.json file", + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema identifier for this adagents.json file" + }, + "authoritative_location": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp indicating when this reference was last updated" + } + }, + "required": [ + "authoritative_location" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Inline structure variant - contains full agent authorization data", + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema identifier for this adagents.json file" + }, + "contact": { + "type": "object", + "description": "Contact information for the entity managing this adagents.json file (may be publisher or third-party operator)", + "properties": { + "name": { + "type": "string", + "description": "Name of the entity managing this file (e.g., 'Meta Advertising Operations', 'Clear Channel Digital')", + "minLength": 1, + "maxLength": 255 + }, + "email": { + "type": "string", + "format": "email", + "description": "Contact email for questions or issues with this authorization file", + "minLength": 1, + "maxLength": 255 + }, + "domain": { + "type": "string", + "description": "Primary domain of the entity managing this file", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "seller_id": { + "type": "string", + "description": "Seller ID from IAB Tech Lab sellers.json (if applicable)", + "minLength": 1, + "maxLength": 255 + }, + "tag_id": { + "type": "string", + "description": "TAG Certified Against Fraud ID for verification (if applicable)", + "minLength": 1, + "maxLength": 100 + }, + "privacy_policy_url": { + "type": "string", + "format": "uri", + "description": "URL to the entity's privacy policy. Used for consumer consent flows when interacting with this sales agent." + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "catalog_etag": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "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." + }, + "properties": { + "type": "array", + "description": "Array of all properties covered by this adagents.json file. Defines the canonical property list that authorized agents reference.", + "items": { + "$ref": "core/property.json" + }, + "minItems": 1 + }, + "revoked_publisher_domains": { + "type": "array", + "description": "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 \u2014 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.", + "items": { + "type": "object", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Publisher domain being revoked. Matches against the same canonicalized form used in `publisher_properties[].publisher_domain`.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "revoked_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this publisher was revoked. Validators MAY use this to order revocations against their own cached state." + }, + "reason": { + "type": "string", + "enum": [ + "relationship_ended", + "compliance_violation", + "publisher_request", + "other" + ], + "description": "Reason for revocation. **Operator-internal self-classification for review routing \u2014 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." + } + }, + "required": [ + "publisher_domain", + "revoked_at" + ], + "additionalProperties": true + } + }, + "collections": { + "type": "array", + "description": "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.", + "items": { + "$ref": "core/collection.json" + } + }, + "placements": { + "type": "array", + "description": "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.", + "items": { + "$ref": "core/placement-definition.json" + }, + "minItems": 1 + }, + "formats": { + "type": "array", + "description": "Publisher-authoritative format catalog. Declares the 3.1+ canonical format-option shapes the publisher supports across its properties \u2014 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 \u2014 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.", + "items": { + "allOf": [ + { + "$ref": "core/product-format-declaration.json" + }, + { + "type": "object", + "properties": { + "applies_to_property_ids": { + "type": "array", + "description": "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.", + "items": { + "$ref": "core/property-id.json" + }, + "minItems": 1 + }, + "applies_to_property_tags": { + "type": "array", + "description": "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` \u2014 a property is in scope if it matches either ID list or tag list.", + "items": { + "$ref": "core/property-tag.json" + }, + "minItems": 1 + } + } + } + ] + }, + "minItems": 1 + }, + "superseded_by": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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 \u22651 minor release after platform adoption so buyer caches keyed on the mirror URL get an explicit migration signal rather than a silent break." + }, + "tags": { + "type": "object", + "description": "Metadata for each tag referenced by properties. Provides human-readable context for property tag values.", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Human-readable name for this tag" + }, + "description": { + "type": "string", + "description": "Description of what this tag represents" + } + }, + "required": [ + "name", + "description" + ], + "additionalProperties": true + } + }, + "placement_tags": { + "type": "object", + "description": "Metadata for each tag referenced by placements. Provides human-readable context for publisher-defined placement tag values used in grouping and authorization.", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Human-readable name for this placement tag" + }, + "description": { + "type": "string", + "description": "Description of what this placement tag represents" + } + }, + "required": [ + "name", + "description" + ], + "additionalProperties": true + } + }, + "authorized_agents": { + "type": "array", + "description": "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.", + "items": { + "discriminator": { + "propertyName": "authorization_type" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "authorization_type": { + "type": "string", + "const": "property_ids", + "description": "Discriminator indicating authorization by specific property IDs" + }, + "property_ids": { + "type": "array", + "description": "Property IDs this agent is authorized for. Resolved against the top-level properties array in this file", + "items": { + "$ref": "core/property-id.json" + }, + "minItems": 1 + }, + "collections": { + "type": "array", + "description": "Optional collection constraints. When present, authorization only applies to inventory associated with these collections.", + "items": { + "$ref": "core/collection-selector.json" + }, + "minItems": 1 + }, + "placement_ids": { + "type": "array", + "description": "Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "placement_tags": { + "type": "array", + "description": "Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "delegation_type": { + "type": "string", + "enum": [ + "direct", + "delegated", + "ad_network" + ], + "description": "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." + }, + "exclusive": { + "type": "boolean", + "description": "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." + }, + "countries": { + "type": "array", + "description": "Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1, + "uniqueItems": true + }, + "effective_from": { + "type": "string", + "format": "date-time", + "description": "Optional start time for this authorization window." + }, + "effective_until": { + "type": "string", + "format": "date-time", + "description": "Optional end time for this authorization window." + } + }, + "required": [ + "authorization_type", + "property_ids" + ], + "additionalProperties": true, + "allOf": [ + { + "$ref": "core/authorized-agent-base.json" + } + ] + }, + { + "type": "object", + "properties": { + "authorization_type": { + "type": "string", + "const": "property_tags", + "description": "Discriminator indicating authorization by property tags" + }, + "property_tags": { + "type": "array", + "description": "Tags identifying which properties this agent is authorized for. Resolved against the top-level properties array in this file using tag matching", + "items": { + "$ref": "core/property-tag.json" + }, + "minItems": 1 + }, + "collections": { + "type": "array", + "description": "Optional collection constraints. When present, authorization only applies to inventory associated with these collections.", + "items": { + "$ref": "core/collection-selector.json" + }, + "minItems": 1 + }, + "placement_ids": { + "type": "array", + "description": "Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "placement_tags": { + "type": "array", + "description": "Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "delegation_type": { + "type": "string", + "enum": [ + "direct", + "delegated", + "ad_network" + ], + "description": "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." + }, + "exclusive": { + "type": "boolean", + "description": "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." + }, + "countries": { + "type": "array", + "description": "Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1, + "uniqueItems": true + }, + "effective_from": { + "type": "string", + "format": "date-time", + "description": "Optional start time for this authorization window." + }, + "effective_until": { + "type": "string", + "format": "date-time", + "description": "Optional end time for this authorization window." + } + }, + "required": [ + "authorization_type", + "property_tags" + ], + "additionalProperties": true, + "allOf": [ + { + "$ref": "core/authorized-agent-base.json" + } + ] + }, + { + "type": "object", + "properties": { + "authorization_type": { + "type": "string", + "const": "inline_properties", + "description": "Discriminator indicating authorization by inline property definitions. Companion field is `properties` (not `inline_properties`) \u2014 the only authorization_type whose companion field name does not mirror the discriminator value." + }, + "properties": { + "type": "array", + "description": "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\"` \u2014 the field is named `properties`, not `inline_properties`.", + "items": { + "$ref": "core/property.json" + }, + "minItems": 1 + }, + "collections": { + "type": "array", + "description": "Optional collection constraints. When present, authorization only applies to inventory associated with these collections.", + "items": { + "$ref": "core/collection-selector.json" + }, + "minItems": 1 + }, + "placement_ids": { + "type": "array", + "description": "Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "placement_tags": { + "type": "array", + "description": "Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "delegation_type": { + "type": "string", + "enum": [ + "direct", + "delegated", + "ad_network" + ], + "description": "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." + }, + "exclusive": { + "type": "boolean", + "description": "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." + }, + "countries": { + "type": "array", + "description": "Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1, + "uniqueItems": true + }, + "effective_from": { + "type": "string", + "format": "date-time", + "description": "Optional start time for this authorization window." + }, + "effective_until": { + "type": "string", + "format": "date-time", + "description": "Optional end time for this authorization window." + } + }, + "required": [ + "authorization_type", + "properties" + ], + "additionalProperties": true, + "allOf": [ + { + "$ref": "core/authorized-agent-base.json" + } + ] + }, + { + "type": "object", + "properties": { + "authorization_type": { + "type": "string", + "const": "publisher_properties", + "description": "Discriminator indicating authorization for properties from other publisher domains" + }, + "publisher_properties": { + "type": "array", + "description": "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", + "items": { + "$ref": "core/publisher-property-selector.json" + }, + "minItems": 1 + }, + "collections": { + "type": "array", + "description": "Optional collection constraints. When present, authorization only applies to inventory associated with these collections.", + "items": { + "$ref": "core/collection-selector.json" + }, + "minItems": 1 + }, + "placement_ids": { + "type": "array", + "description": "Optional placement constraints. When present, authorization only applies to these placement IDs from the top-level placements array in this file.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "placement_tags": { + "type": "array", + "description": "Optional placement tag constraints. When present, authorization only applies to placements whose tags include any of these publisher-defined values.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "delegation_type": { + "type": "string", + "enum": [ + "direct", + "delegated", + "ad_network" + ], + "description": "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." + }, + "exclusive": { + "type": "boolean", + "description": "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." + }, + "countries": { + "type": "array", + "description": "Optional ISO 3166-1 alpha-2 country codes limiting where this authorization applies. Omit for worldwide authorization.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1, + "uniqueItems": true + }, + "effective_from": { + "type": "string", + "format": "date-time", + "description": "Optional start time for this authorization window." + }, + "effective_until": { + "type": "string", + "format": "date-time", + "description": "Optional end time for this authorization window." + } + }, + "required": [ + "authorization_type", + "publisher_properties" + ], + "additionalProperties": true, + "allOf": [ + { + "$ref": "core/authorized-agent-base.json" + } + ] + }, + { + "type": "object", + "description": "Authorization for signals by specific signal IDs", + "properties": { + "authorization_type": { + "type": "string", + "const": "signal_ids", + "description": "Discriminator indicating authorization by specific signal IDs" + }, + "signal_ids": { + "type": "array", + "description": "Signal IDs this agent is authorized to resell. Resolved against the top-level signals array in this file", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "minItems": 1 + } + }, + "required": [ + "authorization_type", + "signal_ids" + ], + "additionalProperties": true, + "allOf": [ + { + "$ref": "core/authorized-agent-base.json" + } + ] + }, + { + "type": "object", + "description": "Authorization for signals by tag membership", + "properties": { + "authorization_type": { + "type": "string", + "const": "signal_tags", + "description": "Discriminator indicating authorization by signal tags" + }, + "signal_tags": { + "type": "array", + "description": "Signal tags this agent is authorized for. Agent can resell all signals with these tags", + "items": { + "type": "string", + "pattern": "^[a-z0-9_-]+$" + }, + "minItems": 1 + } + }, + "required": [ + "authorization_type", + "signal_tags" + ], + "additionalProperties": true, + "allOf": [ + { + "$ref": "core/authorized-agent-base.json" + } + ] + } + ] + }, + "minItems": 1 + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp indicating when this file was last updated" + }, + "property_features": { + "type": "array", + "description": "[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.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "description": "Human-readable name of the vendor/agent (e.g., 'Scope3', 'TAG', 'OneTrust')" + }, + "features": { + "type": "array", + "description": "Feature IDs this agent provides (e.g., 'carbon_score', 'tag_certified_against_fraud'). Use get_adcp_capabilities on the agent for full definitions.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "publisher_id": { + "type": "string", + "description": "Optional publisher identifier at this agent (for lookup)" + } + }, + "required": [ + "url", + "name", + "features" + ], + "additionalProperties": true + } + }, + "signals": { + "type": "array", + "description": "Signal catalog 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.", + "items": { + "$ref": "core/signal-definition.json" + }, + "minItems": 1 + }, + "signal_tags": { + "type": "object", + "description": "Metadata for each tag referenced by signals. Provides human-readable context for signal tag values.", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Human-readable name for this tag" + }, + "description": { + "type": "string", + "description": "Description of what this tag represents" + } + }, + "required": [ + "name", + "description" + ], + "additionalProperties": true + } + } + }, + "required": [ + "authorized_agents" + ], + "additionalProperties": true + } + ], + "examples": [ + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "authoritative_location": "https://cdn.example.com/adagents/v2/adagents.json", + "last_updated": "2025-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "properties": [ + { + "property_id": "example_site", + "property_type": "website", + "name": "Example Site", + "identifiers": [ + { + "type": "domain", + "value": "example.com" + } + ], + "publisher_domain": "example.com" + } + ], + "placements": [ + { + "placement_id": "homepage_banner", + "name": "Homepage Banner", + "tags": [ + "homepage", + "display", + "premium" + ], + "property_ids": [ + "example_site" + ], + "format_options": [ + { + "format_kind": "image", + "params": { + "width": 728, + "height": 90 + } + } + ] + } + ], + "placement_tags": { + "homepage": { + "name": "Homepage", + "description": "Placements that render on the homepage" + }, + "display": { + "name": "Display", + "description": "Standard display placements" + }, + "premium": { + "name": "Premium", + "description": "Premium monetization placements" + } + }, + "authorized_agents": [ + { + "url": "https://agent.example.com", + "authorized_for": "Official sales agent", + "authorization_type": "property_tags", + "property_tags": [ + "all" + ], + "placement_ids": [ + "homepage_banner" + ], + "delegation_type": "direct", + "exclusive": true, + "countries": [ + "US", + "CA" + ], + "effective_from": "2025-01-01T00:00:00Z" + } + ], + "tags": { + "all": { + "name": "All Properties", + "description": "All properties in this file" + } + }, + "last_updated": "2025-01-10T12:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "contact": { + "name": "Meta Advertising Operations", + "email": "adops@meta.com", + "domain": "meta.com", + "seller_id": "pub-meta-12345", + "tag_id": "12345", + "privacy_policy_url": "https://www.meta.com/privacy/policy" + }, + "properties": [ + { + "property_type": "mobile_app", + "name": "Instagram", + "identifiers": [ + { + "type": "ios_bundle", + "value": "com.burbn.instagram" + }, + { + "type": "android_package", + "value": "com.instagram.android" + } + ], + "tags": [ + "meta_network", + "social_media" + ], + "supported_channels": [ + "social", + "display", + "olv" + ], + "publisher_domain": "instagram.com" + }, + { + "property_type": "mobile_app", + "name": "Facebook", + "identifiers": [ + { + "type": "ios_bundle", + "value": "com.facebook.Facebook" + }, + { + "type": "android_package", + "value": "com.facebook.katana" + } + ], + "tags": [ + "meta_network", + "social_media" + ], + "supported_channels": [ + "social", + "display", + "olv" + ], + "publisher_domain": "facebook.com" + }, + { + "property_type": "mobile_app", + "name": "WhatsApp", + "identifiers": [ + { + "type": "ios_bundle", + "value": "net.whatsapp.WhatsApp" + }, + { + "type": "android_package", + "value": "com.whatsapp" + } + ], + "tags": [ + "meta_network", + "messaging" + ], + "supported_channels": [ + "social", + "display" + ], + "publisher_domain": "whatsapp.com" + } + ], + "tags": { + "meta_network": { + "name": "Meta Network", + "description": "All Meta-owned properties" + }, + "social_media": { + "name": "Social Media Apps", + "description": "Social networking applications" + }, + "messaging": { + "name": "Messaging Apps", + "description": "Messaging and communication apps" + } + }, + "authorized_agents": [ + { + "url": "https://meta-ads.com", + "authorized_for": "All Meta properties", + "authorization_type": "property_tags", + "property_tags": [ + "meta_network" + ] + } + ], + "last_updated": "2025-01-10T15:30:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "contact": { + "name": "Tumblr Advertising" + }, + "properties": [ + { + "property_type": "website", + "name": "Tumblr Corporate", + "identifiers": [ + { + "type": "domain", + "value": "tumblr.com" + } + ], + "tags": [ + "corporate" + ], + "publisher_domain": "tumblr.com" + } + ], + "tags": { + "corporate": { + "name": "Corporate Properties", + "description": "Tumblr-owned corporate properties (not user blogs)" + } + }, + "authorized_agents": [ + { + "url": "https://tumblr-sales.com", + "authorized_for": "Tumblr corporate properties only", + "authorization_type": "property_tags", + "property_tags": [ + "corporate" + ] + } + ], + "last_updated": "2025-01-10T16:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "contact": { + "name": "Example Third-Party Sales Agent", + "email": "sales@agent.example", + "domain": "agent.example" + }, + "authorized_agents": [ + { + "url": "https://agent.example/api", + "authorized_for": "CNN CTV properties via publisher authorization", + "authorization_type": "publisher_properties", + "publisher_properties": [ + { + "publisher_domain": "cnn.com", + "selection_type": "by_id", + "property_ids": [ + "cnn_ctv_app" + ] + } + ] + }, + { + "url": "https://agent.example/api", + "authorized_for": "All CTV properties from multiple publishers", + "authorization_type": "publisher_properties", + "publisher_properties": [ + { + "publisher_domain": "cnn.com", + "selection_type": "by_tag", + "property_tags": [ + "ctv" + ] + }, + { + "publisher_domain": "espn.com", + "selection_type": "by_tag", + "property_tags": [ + "ctv" + ] + } + ] + }, + { + "url": "https://agent.example/api", + "authorized_for": "Managed-network display inventory across represented publishers (compact form)", + "authorization_type": "publisher_properties", + "publisher_properties": [ + { + "publisher_domains": [ + "site1.example", + "site2.example", + "site3.example" + ], + "selection_type": "by_tag", + "property_tags": [ + "managed_network" + ] + } + ], + "delegation_type": "ad_network" + } + ], + "last_updated": "2025-01-10T17:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "contact": { + "name": "Premium News Publisher", + "email": "adops@news.example.com", + "domain": "news.example.com" + }, + "properties": [ + { + "property_type": "website", + "name": "News Example", + "identifiers": [ + { + "type": "domain", + "value": "news.example.com" + } + ], + "tags": [ + "premium", + "news" + ], + "publisher_domain": "news.example.com" + } + ], + "tags": { + "premium": { + "name": "Premium Properties", + "description": "High-quality, brand-safe properties" + }, + "news": { + "name": "News Properties", + "description": "News and journalism content" + } + }, + "authorized_agents": [ + { + "url": "https://sales.news.example.com", + "authorized_for": "All news properties", + "authorization_type": "property_tags", + "property_tags": [ + "news" + ] + } + ], + "property_features": [ + { + "url": "https://api.scope3.com", + "name": "Scope3", + "features": [ + "carbon_score", + "sustainability_grade" + ], + "publisher_id": "pub_news_12345" + }, + { + "url": "https://api.tagtoday.net", + "name": "TAG", + "features": [ + "tag_certified_against_fraud", + "tag_brand_safety_certified" + ] + }, + { + "url": "https://api.onetrust.com", + "name": "OneTrust", + "features": [ + "gdpr_compliant", + "tcf_registered", + "ccpa_compliant" + ], + "publisher_id": "ot_news_67890" + } + ], + "last_updated": "2025-01-10T18:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/adagents.json", + "contact": { + "name": "Polk Automotive Data", + "email": "partnerships@polk.com", + "domain": "polk.com" + }, + "signals": [ + { + "id": "likely_tesla_buyers", + "name": "Likely Tesla Buyers", + "description": "Consumers modeled as likely to purchase a Tesla in the next 12 months based on vehicle registration, financial, and behavioral data", + "value_type": "binary", + "category": "purchase_intent", + "tags": [ + "automotive", + "premium" + ] + }, + { + "id": "vehicle_ownership", + "name": "Current Vehicle Ownership", + "description": "Current vehicle make owned by the consumer", + "value_type": "categorical", + "category": "ownership", + "allowed_values": [ + "tesla", + "bmw", + "mercedes", + "audi", + "lexus", + "other_luxury", + "non_luxury" + ], + "tags": [ + "automotive" + ] + }, + { + "id": "purchase_propensity", + "name": "Auto Purchase Propensity", + "description": "Likelihood score of purchasing any new vehicle in the next 6 months", + "value_type": "numeric", + "category": "purchase_intent", + "range": { + "min": 0, + "max": 1, + "unit": "score" + }, + "tags": [ + "automotive" + ] + } + ], + "signal_tags": { + "automotive": { + "name": "Automotive Signals", + "description": "Vehicle-related audience segments" + }, + "premium": { + "name": "Premium Signals", + "description": "High-value premium audience segments" + } + }, + "authorized_agents": [ + { + "url": "https://liveramp.com/.well-known/adcp/signals", + "authorized_for": "All Polk automotive signals via LiveRamp", + "authorization_type": "signal_tags", + "signal_tags": [ + "automotive" + ] + }, + { + "url": "https://the-trade-desk.com/.well-known/adcp/signals", + "authorized_for": "Polk premium signals only", + "authorization_type": "signal_ids", + "signal_ids": [ + "likely_tesla_buyers" + ] + } + ], + "last_updated": "2025-01-15T10:00:00Z" + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand.json b/schemas/cache/3.1.0-beta.5/brand.json new file mode 100644 index 000000000..38c74a1e1 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand.json @@ -0,0 +1,2883 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Brand Discovery", + "description": "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.", + "definitions": { + "domain": { + "type": "string", + "description": "A valid domain name", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "type": "string", + "description": "Brand identifier within the house portfolio. Lowercase alphanumeric with underscores. House chooses this ID.", + "pattern": "^[a-z0-9_]+$" + }, + "trademark": { + "type": "object", + "description": "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 \u2014 both lists are valid claims about marks the publisher controls.", + "properties": { + "registry": { + "type": "string", + "description": "Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA')" + }, + "number": { + "type": "string", + "description": "Registration number as issued by the registry" + }, + "mark": { + "type": "string", + "description": "The registered mark as published" + }, + "status": { + "type": "string", + "enum": [ + "active", + "pending", + "abandoned", + "cancelled", + "expired" + ], + "description": "Registration status. Omit for active marks if status tracking is not maintained." + }, + "license_type": { + "type": "string", + "enum": [ + "owned", + "licensed_in", + "licensed_out" + ], + "description": "Whether the publisher owns the mark, licenses it from another entity, or licenses it to others. 'owned' is the default if omitted." + }, + "licensor_domain": { + "$ref": "#/definitions/domain", + "description": "Domain of the entity that licenses this mark to the publisher. Meaningful when license_type=licensed_in; omit otherwise." + }, + "countries": { + "type": "array", + "items": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "description": "ISO 3166-1 alpha-2 country codes where this registration applies. Omit for global or where the registry's jurisdiction is implicit." + }, + "nice_classes": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1, + "maximum": 45 + }, + "description": "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." + } + }, + "required": [ + "registry", + "number", + "mark" + ], + "additionalProperties": true + }, + "portfolio_entry": { + "type": "object", + "description": "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", + "properties": { + "domain": { + "$ref": "#/definitions/domain", + "description": "Domain where the child's canonical brand.json lives" + }, + "brand_id": { + "$ref": "#/definitions/brand_id", + "description": "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": { + "$ref": "#/definitions/domain", + "description": "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." + }, + "effective_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the house established this ownership claim. Consumers age mutual-assertion edges from this date for TTL purposes. Optional; absent means the consumer ages from its own first observation." + } + }, + "required": [ + "domain", + "brand_id" + ], + "additionalProperties": false + }, + "localized_name": { + "type": "object", + "description": "A localized name with BCP 47 locale code key (e.g., 'en_US', 'fr_CA', 'zh_CN') and name value. Bare language codes ('en') are accepted as wildcards for backwards compatibility.", + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "keller_type": { + "type": "string", + "enum": [ + "master", + "sub_brand", + "endorsed", + "independent" + ], + "description": "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.)" + }, + "logo": { + "type": "object", + "description": "Brand logo asset with structured fields for orientation, background compatibility, and variant type", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL to the logo asset" + }, + "orientation": { + "type": "string", + "enum": [ + "square", + "horizontal", + "vertical", + "stacked" + ], + "description": "Logo aspect ratio orientation. square: ~1:1, horizontal: wide, vertical: tall, stacked: vertically arranged elements" + }, + "background": { + "type": "string", + "enum": [ + "dark-bg", + "light-bg", + "transparent-bg" + ], + "description": "Background compatibility. dark-bg: use on dark backgrounds, light-bg: use on light backgrounds, transparent-bg: has transparent background" + }, + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary", + "icon", + "wordmark", + "full-lockup" + ], + "description": "Logo variant type. primary: main logo, secondary: alternative, icon: symbol only, wordmark: text only, full-lockup: complete logo" + }, + "tags": { + "type": "array", + "description": "Additional semantic tags for custom categorization beyond the standard orientation, background, and variant fields", + "items": { + "type": "string" + } + }, + "usage": { + "type": "string", + "description": "Human-readable description of when to use this logo variant (e.g., 'Primary logo for use on light backgrounds')" + }, + "width": { + "type": "integer", + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "description": "Height in pixels" + } + }, + "required": [ + "url" + ], + "additionalProperties": true + }, + "hex_color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$", + "description": "A single hex color value" + }, + "color_value": { + "oneOf": [ + { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "minItems": 1 + } + ] + }, + "colors": { + "type": "object", + "description": "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 \u2014 heading, body, label, border, divider, surface_1, surface_2, etc.", + "properties": { + "primary": { + "$ref": "#/definitions/color_value" + }, + "secondary": { + "$ref": "#/definitions/color_value" + }, + "accent": { + "$ref": "#/definitions/color_value" + }, + "background": { + "$ref": "#/definitions/color_value" + }, + "text": { + "$ref": "#/definitions/color_value" + }, + "heading": { + "$ref": "#/definitions/color_value" + }, + "body": { + "$ref": "#/definitions/color_value" + }, + "label": { + "$ref": "#/definitions/color_value" + }, + "border": { + "$ref": "#/definitions/color_value" + }, + "divider": { + "$ref": "#/definitions/color_value" + }, + "surface_1": { + "$ref": "#/definitions/color_value" + }, + "surface_2": { + "$ref": "#/definitions/color_value" + } + }, + "additionalProperties": { + "$ref": "#/definitions/color_value" + } + }, + "font_file": { + "type": "object", + "description": "A font file with weight, style, and variable font metadata", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL to the font file (WOFF2, TTF, or OTF)" + }, + "weight": { + "type": "integer", + "minimum": 100, + "maximum": 900, + "description": "CSS numeric font-weight for static fonts (100-900)" + }, + "weight_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 100, + "maximum": 900 + }, + "minItems": 2, + "maxItems": 2, + "description": "Variable font weight axis range as [min, max] (e.g., [100, 900]). Use instead of weight for variable fonts." + }, + "style": { + "type": "string", + "enum": [ + "normal", + "italic", + "oblique" + ], + "description": "CSS font-style" + } + }, + "required": [ + "url" + ], + "additionalProperties": true + }, + "font_role": { + "description": "A font role entry. Either a CSS font-family string (simple) or a structured object with family name and font files (rich).", + "oneOf": [ + { + "type": "string", + "description": "CSS font-family name (e.g., 'Montserrat', 'Arial, sans-serif')" + }, + { + "type": "object", + "description": "Structured font with family name, downloadable font files, and typographic metadata", + "properties": { + "family": { + "type": "string", + "description": "CSS font-family name (e.g., 'Brand Sans')" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/font_file" + }, + "maxItems": 36, + "description": "Font files for different weights and styles" + }, + "opentype_features": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]{4}$" + }, + "maxItems": 20, + "description": "OpenType feature tags to enable (e.g., ['ss01', 'tnum', 'cv01']). These are four-character tags per the OpenType spec." + }, + "fallbacks": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "maxItems": 10, + "description": "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'])" + } + }, + "required": [ + "family" + ], + "additionalProperties": true + } + ] + }, + "fonts": { + "type": "object", + "description": "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.", + "maxProperties": 20, + "properties": { + "primary": { + "$ref": "#/definitions/font_role", + "description": "Primary font family" + }, + "secondary": { + "$ref": "#/definitions/font_role", + "description": "Secondary font family" + } + }, + "additionalProperties": { + "$ref": "#/definitions/font_role" + } + }, + "asset": { + "type": "object", + "description": "Brand asset (image, video, audio, text)", + "properties": { + "asset_id": { + "type": "string", + "description": "Unique identifier" + }, + "asset_type": { + "$ref": "enums/asset-content-type.json", + "description": "Type of asset content" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to CDN-hosted asset file" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for discovery (e.g., 'hero', 'lifestyle', 'product', 'holiday')" + }, + "name": { + "type": "string", + "description": "Human-readable name" + }, + "description": { + "type": "string", + "description": "Asset description or usage notes" + }, + "width": { + "type": "integer", + "description": "Image/video width in pixels" + }, + "height": { + "type": "integer", + "description": "Image/video height in pixels" + }, + "duration_seconds": { + "type": "number", + "description": "Video/audio duration in seconds" + }, + "file_size_bytes": { + "type": "integer", + "description": "File size in bytes" + }, + "format": { + "type": "string", + "description": "File format (e.g., 'jpg', 'mp4', 'mp3')" + }, + "metadata": { + "type": "object", + "description": "Additional asset-specific metadata", + "additionalProperties": true + } + }, + "required": [ + "asset_id", + "asset_type", + "url" + ], + "additionalProperties": true + }, + "property": { + "type": "object", + "description": "A digital property associated with a brand. Defaults to owned; use 'relationship' to declare direct, delegated, or ad_network properties. 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.", + "properties": { + "type": { + "type": "string", + "enum": [ + "website", + "mobile_app", + "ctv_app", + "desktop_app", + "dooh", + "podcast", + "radio", + "streaming_audio" + ], + "description": "Property type" + }, + "identifier": { + "type": "string", + "description": "Property identifier - domain for websites, bundle ID for apps", + "minLength": 1 + }, + "store": { + "type": "string", + "enum": [ + "apple", + "google", + "amazon", + "roku", + "samsung", + "lg", + "other" + ], + "description": "App store for mobile/CTV apps" + }, + "region": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code or 'global'", + "pattern": "^([A-Z]{2}|global)$" + }, + "primary": { + "type": "boolean", + "default": false, + "description": "Whether this is the primary property for the brand" + }, + "relationship": { + "type": "string", + "enum": [ + "owned", + "direct", + "delegated", + "ad_network" + ], + "default": "owned", + "description": "How this brand relates to the property, using the same vocabulary as adagents.json delegation_type. 'owned': the brand owns and operates this property (default). '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 \u2014 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 \u2014 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." + } + }, + "required": [ + "type", + "identifier" + ], + "additionalProperties": true + }, + "product_catalog": { + "type": "object", + "description": "Product catalog for e-commerce brands", + "properties": { + "feed_url": { + "type": "string", + "format": "uri", + "description": "URL to product catalog feed" + }, + "feed_format": { + "type": "string", + "enum": [ + "google_merchant_center", + "facebook_catalog", + "openai_product_feed", + "custom" + ], + "description": "Format of the product feed" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Product categories available in the catalog" + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "When the product catalog was last updated" + }, + "update_frequency": { + "type": "string", + "enum": [ + "realtime", + "hourly", + "daily", + "weekly" + ], + "description": "How frequently the product catalog is updated" + }, + "agentic_checkout": { + "type": "object", + "description": "Agentic checkout endpoint configuration", + "properties": { + "endpoint": { + "type": "string", + "format": "uri", + "description": "Base URL for checkout session API" + }, + "spec": { + "type": "string", + "description": "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": { + "type": "array", + "description": "Payment providers supported by this checkout endpoint", + "items": { + "type": "string" + } + } + }, + "required": [ + "endpoint", + "spec" + ] + } + }, + "required": [ + "feed_url" + ], + "additionalProperties": true + }, + "data_subject_contestation": { + "type": "object", + "description": "Contact point where a data subject can request human intervention, express their view, or contest an automated decision \u2014 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.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL to a human-accessible contestation form or information page." + }, + "email": { + "type": "string", + "format": "email", + "description": "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))." + }, + "languages": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand": { + "type": "object", + "description": "A brand within a house portfolio. Combines identity (who) with creative assets (how to represent). Referenced as domain + brand_id.", + "properties": { + "id": { + "$ref": "#/definitions/brand_id", + "description": "Brand identifier within the house. House chooses this ID." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Primary brand URL for context and asset discovery" + }, + "names": { + "type": "array", + "description": "Localized brand names. Multiple entries per language allowed for aliases.", + "items": { + "$ref": "#/definitions/localized_name" + }, + "minItems": 1 + }, + "keller_type": { + "$ref": "#/definitions/keller_type" + }, + "parent_brand": { + "$ref": "#/definitions/brand_id", + "description": "Parent brand ID for sub-brands and endorsed brands" + }, + "description": { + "type": "string", + "description": "Brand description" + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Brand industries (e.g., ['automotive'] or ['pharmaceutical', 'cpg'] for a consumer health company). Describes what the company does \u2014 not what regulatory regimes apply (use policy_categories for that)." + }, + "target_audience": { + "type": "string", + "description": "Primary target audience" + }, + "logos": { + "type": "array", + "items": { + "$ref": "#/definitions/logo" + }, + "description": "Brand logo assets" + }, + "colors": { + "$ref": "#/definitions/colors" + }, + "fonts": { + "$ref": "#/definitions/fonts" + }, + "tone": { + "description": "Brand voice and messaging tone guidelines", + "oneOf": [ + { + "type": "string", + "description": "Simple tone descriptors for backwards compatibility" + }, + { + "type": "object", + "description": "Structured brand voice guidelines", + "properties": { + "voice": { + "type": "string", + "description": "High-level voice descriptor (e.g., 'warm and inviting', 'professional and trustworthy')" + }, + "attributes": { + "type": "array", + "description": "Personality traits that characterize the brand voice", + "items": { + "type": "string" + } + }, + "dos": { + "type": "array", + "description": "Guidance for copy generation - what TO do", + "items": { + "type": "string" + } + }, + "donts": { + "type": "array", + "description": "Guardrails to avoid brand violations - what NOT to do", + "items": { + "type": "string" + } + } + } + } + ] + }, + "tagline": { + "oneOf": [ + { + "type": "string", + "description": "Plain tagline string for backwards compatibility" + }, + { + "type": "array", + "description": "Localized taglines with BCP 47 locale codes", + "items": { + "$ref": "#/definitions/localized_name" + }, + "minItems": 1 + } + ], + "description": "Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern." + }, + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/asset" + }, + "description": "Brand asset library" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/definitions/property" + }, + "description": "Digital properties associated with this brand \u2014 owned, managed, or represented" + }, + "product_catalog": { + "$ref": "#/definitions/product_catalog" + }, + "privacy_policy_url": { + "type": "string", + "format": "uri", + "description": "URL to the brand's privacy policy" + }, + "data_subject_contestation": { + "$ref": "#/definitions/data_subject_contestation" + }, + "disclaimers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "context": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": true + } + }, + "required": [ + "text" + ] + }, + "description": "Legal disclaimers for creatives" + }, + "trademarks": { + "type": "array", + "items": { + "$ref": "#/definitions/trademark" + }, + "description": "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 \u2014 both lists are valid claims." + }, + "voice_synthesis": { + "type": "object", + "description": "TTS voice synthesis configuration for AI-generated audio", + "properties": { + "provider": { + "type": "string" + }, + "voice_id": { + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "avatar": { + "type": "object", + "description": "Visual avatar configuration", + "properties": { + "provider": { + "type": "string" + }, + "avatar_id": { + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "visual_guidelines": { + "$ref": "#/definitions/visual_guidelines", + "description": "Structured visual rules for generative creative systems" + }, + "agents": { + "$ref": "#/definitions/agents", + "description": "Agents authorized to act on behalf of this brand. Overrides house-level agents of the same type." + }, + "brand_agent": { + "$ref": "#/definitions/brand_agent", + "description": "Deprecated: use agents array with type 'brand' instead. Brand agent that provides dynamic brand data via MCP.", + "deprecated": true + }, + "rights_agent": { + "$ref": "#/definitions/rights_agent", + "description": "Deprecated: use agents array with type 'rights' instead. Rights licensing agent for this brand.", + "deprecated": true + }, + "contact": { + "type": "object", + "description": "Brand-level contact information", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Contact email" + }, + "phone": { + "type": "string", + "description": "Contact phone number" + } + } + }, + "collections": { + "type": "array", + "description": "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.", + "items": { + "type": "object", + "properties": { + "collection_id": { + "type": "string", + "description": "Collection identifier as used in the seller's get_products responses" + }, + "name": { + "type": "string", + "description": "Human-readable collection name" + }, + "role": { + "$ref": "enums/talent-role.json", + "description": "This person's role on the collection" + }, + "seller_agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the sales agent that sells inventory for this collection. Buyer agents can query this agent for collection products." + } + }, + "required": [ + "name" + ], + "additionalProperties": true + } + } + }, + "required": [ + "id", + "names" + ], + "additionalProperties": true + }, + "house": { + "type": "object", + "description": "Corporate or organizational entity that owns brands", + "properties": { + "domain": { + "$ref": "#/definitions/domain", + "description": "The house's domain where brand.json is hosted" + }, + "name": { + "type": "string", + "description": "Primary display name of the house", + "minLength": 1 + }, + "names": { + "type": "array", + "description": "Localized house names including legal name, stock symbol, etc.", + "items": { + "$ref": "#/definitions/localized_name" + } + }, + "architecture": { + "type": "string", + "enum": [ + "branded_house", + "house_of_brands", + "hybrid" + ], + "description": "Brand architecture model: branded_house (Google), house_of_brands (P&G), hybrid (Nike)" + }, + "agents": { + "$ref": "#/definitions/agents", + "description": "House-level agents that apply to all brands unless overridden at the brand level" + }, + "data_subject_contestation": { + "$ref": "#/definitions/data_subject_contestation", + "description": "House-level fallback contestation contact. Governance agents resolve in order: brand.data_subject_contestation \u2192 house.data_subject_contestation \u2192 missing (critical finding when human review required)." + } + }, + "required": [ + "domain", + "name" + ], + "additionalProperties": true + }, + "brand_agent": { + "type": "object", + "description": "Reference to a brand agent that provides brand data via MCP", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "Agent identifier (useful for logging, multi-tenant DAMs)", + "pattern": "^[a-z0-9_]+$" + } + }, + "required": [ + "url", + "id" + ], + "additionalProperties": true + }, + "rights_agent": { + "type": "object", + "description": "Rights licensing agent for this brand. Provides discovery, pricing, and acquisition of licensable rights via MCP. Use get_rights and acquire_rights tasks to interact.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "Agent identifier", + "pattern": "^[a-z0-9_]+$" + }, + "available_uses": { + "type": "array", + "description": "Rights uses available for licensing through this agent", + "items": { + "$ref": "enums/right-use.json" + }, + "minItems": 1 + }, + "right_types": { + "type": "array", + "description": "Types of rights available", + "items": { + "$ref": "enums/right-type.json" + }, + "minItems": 1 + }, + "countries": { + "type": "array", + "description": "Countries where rights are available (ISO 3166-1 alpha-2)", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + } + }, + "required": [ + "url", + "id", + "available_uses" + ], + "additionalProperties": true + }, + "brand_agent_entry": { + "type": "object", + "description": "An agent declared by a brand or house. Each entry identifies one agent endpoint and its functional role in the advertising ecosystem.", + "properties": { + "type": { + "$ref": "enums/brand-agent-type.json", + "description": "Functional role of this agent" + }, + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "Agent identifier (useful for logging, multi-tenant platforms)", + "pattern": "^[a-z0-9_]+$", + "maxLength": 100 + }, + "description": { + "type": "string", + "description": "Human-readable description of this agent's capabilities or scope", + "maxLength": 500 + }, + "jwks_uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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`." + }, + "available_uses": { + "type": "array", + "description": "For rights agents: rights uses available for licensing", + "items": { + "$ref": "enums/right-use.json" + }, + "minItems": 1 + }, + "right_types": { + "type": "array", + "description": "For rights agents: types of rights available", + "items": { + "$ref": "enums/right-type.json" + }, + "minItems": 1 + }, + "countries": { + "type": "array", + "description": "ISO 3166-1 alpha-2 country codes where this agent operates. Omit for global scope.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + } + }, + "required": [ + "type", + "url", + "id" + ], + "additionalProperties": true + }, + "agents": { + "type": "array", + "description": "Agents declared by this brand or house. One agent per type.", + "items": { + "$ref": "#/definitions/brand_agent_entry" + }, + "maxItems": 20 + }, + "authorized_operator": { + "type": "object", + "description": "An entity authorized to represent brands from this house. Verified by resolving the operator's domain.", + "properties": { + "domain": { + "$ref": "#/definitions/domain", + "description": "Domain of the authorized operator (e.g., 'groupm.com')" + }, + "brands": { + "type": "array", + "description": "Brand IDs this operator is authorized for. Use ['*'] for all brands in the portfolio.", + "items": { + "type": "string", + "pattern": "^([a-z0-9_]+|\\*)$" + }, + "minItems": 1 + }, + "countries": { + "type": "array", + "description": "ISO 3166-1 alpha-2 country codes where this authorization applies. Omit for global authorization.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + } + }, + "required": [ + "domain", + "brands" + ], + "additionalProperties": true + }, + "photography_style": { + "type": "object", + "description": "Photography style rules for generative creative systems. Defines how brand photography should look when selected or generated.", + "properties": { + "realism": { + "type": "string", + "enum": [ + "natural", + "stylized", + "hyperreal", + "abstract" + ], + "description": "Level of photographic realism" + }, + "lighting": { + "type": "string", + "description": "Lighting style (e.g., 'soft daylight', 'studio', 'golden hour', 'high-key', 'low-key')" + }, + "color_temperature": { + "type": "string", + "enum": [ + "warm", + "neutral", + "cool" + ], + "description": "Overall color temperature of photography" + }, + "contrast": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "Contrast level in photography" + }, + "depth_of_field": { + "type": "string", + "enum": [ + "shallow", + "medium", + "deep" + ], + "description": "Depth of field preference. shallow: blurred background with subject isolation, deep: everything in focus" + }, + "subject": { + "type": "object", + "description": "Subject matter guidelines", + "properties": { + "people": { + "type": "object", + "description": "People photography guidelines", + "properties": { + "age_range": { + "type": "string", + "description": "Target age range (e.g., '20-35')" + }, + "diversity": { + "type": "string", + "description": "Diversity representation (e.g., 'mixed', 'varied')" + }, + "mood": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Mood descriptors (e.g., ['confident', 'relaxed'])" + } + }, + "additionalProperties": true + }, + "product_focus": { + "type": "string", + "enum": [ + "in-use", + "isolated", + "lifestyle", + "detail" + ], + "description": "How products are shown" + }, + "setting": { + "type": "string", + "description": "Environmental context for photography (e.g., 'indoor', 'outdoor', 'studio', 'urban', 'nature', 'workplace')" + } + }, + "additionalProperties": true + }, + "framing": { + "type": "object", + "description": "Camera framing rules", + "properties": { + "subject_position": { + "type": "string", + "description": "Where the subject sits in frame (e.g., 'center', 'center-left', 'rule-of-thirds')" + }, + "crop_style": { + "type": "string", + "description": "Cropping convention (e.g., 'waist-up', 'full-body', 'close-up', 'wide')" + }, + "perspective": { + "type": "string", + "description": "Camera perspective (e.g., 'eye-level', 'overhead', 'low-angle')" + } + }, + "additionalProperties": true + }, + "preferred_aspect_ratios": { + "type": "array", + "items": { + "type": "string", + "pattern": "^\\d+:\\d+$" + }, + "description": "Preferred aspect ratios for brand photography (e.g., '16:9', '4:5', '1:1')" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional style descriptors" + } + }, + "additionalProperties": true + }, + "graphic_style": { + "type": "object", + "description": "Visual language for brand graphics and illustrations", + "properties": { + "style_type": { + "type": "string", + "enum": [ + "flat_illustration", + "geometric", + "gradient_mesh", + "editorial_collage", + "hand_drawn", + "minimal_line_art", + "3d_render", + "isometric", + "photographic_composite" + ], + "description": "Primary graphic style" + }, + "stroke_style": { + "type": "string", + "enum": [ + "rounded", + "square", + "mixed", + "none" + ], + "description": "Stroke end/join style" + }, + "stroke_weight": { + "type": "string", + "description": "Stroke weight (e.g., '2px', 'thin', 'bold')" + }, + "corner_radius": { + "type": "string", + "description": "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." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional style descriptors" + } + }, + "additionalProperties": true + }, + "border_radius": { + "type": "object", + "description": "Named border radius presets for UI components and layout elements. One of the most visible brand differentiators \u2014 Airbnb uses generous 20px, Stripe uses precise 4\u20138px, Spotify uses pill/999px.", + "properties": { + "none": { + "type": "string", + "description": "Explicitly sharp corners (e.g., '0')" + }, + "default": { + "type": "string", + "description": "Default border radius for UI components (e.g., '8px', '12px', '0'). For graphic/illustration elements, see graphic_style.corner_radius." + }, + "small": { + "type": "string", + "description": "Small border radius for compact elements (e.g., '4px')" + }, + "large": { + "type": "string", + "description": "Large border radius for cards and containers (e.g., '16px', '24px')" + }, + "pill": { + "type": "string", + "description": "Fully rounded / pill shape (e.g., '999px')" + } + }, + "additionalProperties": { + "type": "string" + } + }, + "elevation": { + "type": "object", + "description": "Named shadow/elevation levels. Brands use elevation as identity \u2014 from Stripe's blue-tinted multi-layer shadows to Apple's single diffuse shadow. Values are CSS box-shadow syntax.", + "properties": { + "none": { + "type": "string", + "description": "No shadow (e.g., 'none')" + }, + "subtle": { + "type": "string", + "description": "Subtle shadow for slight lift (e.g., '0 1px 2px rgba(0,0,0,0.05)')" + }, + "card": { + "type": "string", + "description": "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)')" + }, + "modal": { + "type": "string", + "description": "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)')" + } + }, + "additionalProperties": { + "type": "string" + } + }, + "spacing": { + "type": "object", + "description": "Spacing system for consistent layout rhythm. Most design systems use an 8px base grid.", + "properties": { + "unit": { + "type": "string", + "description": "Base grid unit this scale was designed from (e.g., '8px', '4px'). Informational \u2014 agents should use the named scale values, not compute from this." + }, + "scale": { + "type": "object", + "description": "Named spacing scale built from the base unit", + "properties": { + "xs": { + "type": "string", + "description": "Extra small spacing (e.g., '4px')" + }, + "sm": { + "type": "string", + "description": "Small spacing (e.g., '8px')" + }, + "md": { + "type": "string", + "description": "Medium spacing (e.g., '16px')" + }, + "lg": { + "type": "string", + "description": "Large spacing (e.g., '24px')" + }, + "xl": { + "type": "string", + "description": "Extra large spacing (e.g., '32px')" + }, + "2xl": { + "type": "string", + "description": "Section-level spacing (e.g., '48px', '64px')" + } + }, + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "brand_shapes": { + "type": "object", + "description": "Distinctive shapes used as part of brand visual identity", + "properties": { + "primary_shape": { + "type": "string", + "description": "Primary brand shape (e.g., 'rounded_rectangle', 'circle', 'hexagon')" + }, + "secondary_shapes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Secondary shapes in the brand vocabulary" + }, + "usage": { + "type": "object", + "description": "Shape usage rules", + "properties": { + "max_per_layout": { + "type": "integer", + "description": "Maximum distinct shapes per layout" + }, + "overlap_allowed": { + "type": "boolean", + "description": "Whether shapes may overlap" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "iconography": { + "type": "object", + "description": "Icon style system and usage rules", + "properties": { + "style": { + "type": "string", + "enum": [ + "outline", + "filled", + "duotone", + "flat", + "glyph", + "hand_drawn" + ], + "description": "Icon rendering style" + }, + "stroke_weight": { + "type": "string", + "description": "Icon stroke weight (e.g., '2px', '1.5px')" + }, + "corner_style": { + "type": "string", + "enum": [ + "rounded", + "square", + "mixed" + ], + "description": "Corner style for icon paths" + }, + "usage": { + "type": "object", + "description": "Icon usage rules", + "properties": { + "max_per_frame": { + "type": "integer", + "description": "Maximum icons per creative frame" + }, + "size_ratio": { + "type": "string", + "description": "Icon-to-layout size ratio (e.g., '1:8')" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "composition_rules": { + "type": "object", + "description": "Layout composition rules including overlays, textures, and backgrounds", + "properties": { + "overlays": { + "type": "object", + "description": "Graphic overlay rules", + "properties": { + "gradient_style": { + "type": "string", + "enum": [ + "linear", + "radial", + "conic", + "none" + ], + "description": "Gradient type for overlays" + }, + "gradient_direction": { + "type": "string", + "description": "Gradient direction (e.g., '45deg', 'to-bottom-right')" + }, + "opacity": { + "type": "string", + "description": "Overlay opacity (e.g., '70%')" + } + }, + "additionalProperties": true + }, + "texture": { + "type": "object", + "description": "Texture treatment rules", + "properties": { + "style": { + "type": "string", + "enum": [ + "none", + "subtle_grain", + "noise", + "paper", + "fabric", + "concrete" + ], + "description": "Texture style applied to creative assets" + }, + "intensity": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "Texture intensity" + } + }, + "additionalProperties": true + }, + "backgrounds": { + "type": "object", + "description": "Background treatment rules", + "properties": { + "types_allowed": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "solid_color", + "gradient", + "blurred_photo", + "image", + "video", + "pattern", + "transparent" + ] + }, + "description": "Permitted background types" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "colorway": { + "type": "object", + "description": "A named color pairing that defines how colors work together. Colorways ensure foreground/background combinations are always on-brand and accessible.", + "properties": { + "name": { + "type": "string", + "description": "Colorway name (e.g., 'primary', 'inverted', 'subtle')" + }, + "foreground": { + "$ref": "#/definitions/hex_color" + }, + "background": { + "$ref": "#/definitions/hex_color" + }, + "accent": { + "$ref": "#/definitions/hex_color" + }, + "border": { + "$ref": "#/definitions/hex_color" + }, + "cta_foreground": { + "$ref": "#/definitions/hex_color", + "description": "CTA text/icon color, if different from foreground" + }, + "cta_background": { + "$ref": "#/definitions/hex_color", + "description": "CTA button/container color, if different from accent" + }, + "channels": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Channels or contexts where this colorway applies (e.g., 'online', 'print', 'pos', 'social', 'outdoor'). Omit for universal colorways." + } + }, + "required": [ + "name", + "foreground", + "background" + ], + "additionalProperties": true + }, + "type_scale_entry": { + "type": "object", + "description": "A single entry in the type scale", + "properties": { + "font": { + "type": "string", + "description": "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." + }, + "size": { + "type": "string", + "description": "Font size (e.g., '48px', '2rem')" + }, + "weight": { + "type": "string", + "description": "Font weight (e.g., '700', 'bold')" + }, + "line_height": { + "type": "string", + "description": "Line height (e.g., '1.2', '56px')" + }, + "letter_spacing": { + "type": "string", + "description": "Letter spacing (e.g., '-0.02em', '0.5px')" + }, + "text_transform": { + "type": "string", + "enum": [ + "none", + "uppercase", + "lowercase", + "capitalize" + ], + "description": "Text transformation" + } + }, + "additionalProperties": true + }, + "motion_guidelines": { + "type": "object", + "description": "Motion and animation rules for video, animated display, and interactive formats", + "properties": { + "transition_style": { + "type": "string", + "enum": [ + "cut", + "dissolve", + "slide", + "wipe", + "zoom", + "fade" + ], + "description": "Primary transition style between scenes" + }, + "animation_speed": { + "type": "string", + "enum": [ + "slow", + "moderate", + "fast" + ], + "description": "Overall animation pacing" + }, + "easing": { + "type": "string", + "description": "Default easing function (e.g., 'ease-in-out', 'spring', 'linear')" + }, + "text_entrance": { + "type": "string", + "enum": [ + "fade", + "typewriter", + "slide_up", + "slide_left", + "scale", + "none" + ], + "description": "How text enters the frame" + }, + "pacing": { + "type": "string", + "enum": [ + "lingering", + "moderate", + "fast_cuts" + ], + "description": "Overall editing rhythm" + }, + "kinetic_typography": { + "type": "boolean", + "description": "Whether animated/kinetic typography is allowed" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional motion style descriptors" + } + }, + "additionalProperties": true + }, + "logo_placement": { + "type": "object", + "description": "Logo placement and clear space rules for automated creative production", + "properties": { + "preferred_position": { + "type": "string", + "enum": [ + "top-left", + "top-center", + "top-right", + "bottom-left", + "bottom-center", + "bottom-right", + "center" + ], + "description": "Preferred logo position in layouts" + }, + "min_clear_space": { + "type": "string", + "description": "Minimum clear space around the logo, expressed as a multiple of logo height (e.g., '0.5x', '1x') or fixed value (e.g., '16px')" + }, + "min_height": { + "type": "string", + "description": "Minimum logo height to maintain legibility (e.g., '40px', '24px')" + }, + "background_contrast": { + "type": "string", + "enum": [ + "light_only", + "dark_only", + "any" + ], + "description": "Permitted background contrast behind logo" + } + }, + "additionalProperties": true + }, + "graphic_element": { + "type": "object", + "description": "A reusable decorative or structural visual element that is part of the brand identity (e.g., torn paper edges, watermarks, dividers, background patterns)", + "properties": { + "name": { + "type": "string", + "description": "Element name (e.g., 'Paper Tear', 'Brand Watermark', 'Section Divider')" + }, + "type": { + "type": "string", + "enum": [ + "border", + "divider", + "frame", + "watermark", + "pattern", + "texture_overlay", + "decorative" + ], + "description": "Element type" + }, + "description": { + "type": "string", + "description": "How the element is used in layouts" + }, + "orientation": { + "type": "string", + "enum": [ + "horizontal", + "vertical", + "any" + ], + "description": "Preferred orientation when used in layouts" + }, + "colors": { + "type": "array", + "items": { + "$ref": "#/definitions/hex_color" + }, + "description": "Colors this element may appear in" + }, + "max_per_layout": { + "type": "integer", + "description": "Maximum instances per layout" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "asset_library": { + "type": "object", + "description": "A managed asset library (icon set, illustration system, image collection). The URL is for human access; agent-facing DAM integration is under investigation.", + "properties": { + "name": { + "type": "string", + "description": "Display name of the asset library" + }, + "type": { + "type": "string", + "enum": [ + "icon_set", + "illustration_system", + "image_library", + "video_library", + "template_library" + ], + "description": "Type of asset library" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the asset library (for human access)" + }, + "description": { + "type": "string", + "description": "Description of the library contents and usage" + }, + "color_guide": { + "type": "object", + "description": "Color guide for the asset library defining roles and palettes", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Named color roles used in the library (e.g., base, shadow_1, highlight_1, stroke)" + }, + "palettes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Palette name" + }, + "colors": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/hex_color" + }, + "description": "Map of role names to hex color values" + } + }, + "required": [ + "name", + "colors" + ], + "additionalProperties": true + }, + "description": "Named color palettes mapping roles to specific colors" + } + }, + "additionalProperties": true + } + }, + "required": [ + "name", + "url" + ], + "additionalProperties": true + }, + "visual_guidelines": { + "type": "object", + "description": "Structured visual rules for generative creative systems. Defines how brand photography, graphics, typography, and composition should be produced to maintain brand consistency at scale.", + "properties": { + "photography": { + "$ref": "#/definitions/photography_style" + }, + "graphic_style": { + "$ref": "#/definitions/graphic_style" + }, + "shapes": { + "$ref": "#/definitions/brand_shapes" + }, + "iconography": { + "$ref": "#/definitions/iconography" + }, + "composition": { + "$ref": "#/definitions/composition_rules" + }, + "border_radius": { + "$ref": "#/definitions/border_radius" + }, + "elevation": { + "$ref": "#/definitions/elevation" + }, + "spacing": { + "$ref": "#/definitions/spacing" + }, + "graphic_elements": { + "type": "array", + "items": { + "$ref": "#/definitions/graphic_element" + }, + "description": "Reusable decorative elements that are part of the brand visual identity (e.g., torn paper edges, watermarks, dividers)" + }, + "motion": { + "$ref": "#/definitions/motion_guidelines" + }, + "logo_placement": { + "$ref": "#/definitions/logo_placement" + }, + "colorways": { + "type": "array", + "items": { + "$ref": "#/definitions/colorway" + }, + "description": "Named color pairings for consistent foreground/background combinations" + }, + "type_scale": { + "type": "object", + "description": "Typography scale defining sizes and weights for different text roles. When sizes are in px, use base_width to indicate the reference canvas.", + "properties": { + "base_width": { + "type": "string", + "description": "Reference canvas width these sizes were designed for (e.g., '1080px'). Generative systems should scale proportionally for other canvas sizes." + }, + "heading": { + "$ref": "#/definitions/type_scale_entry" + }, + "subheading": { + "$ref": "#/definitions/type_scale_entry" + }, + "body": { + "$ref": "#/definitions/type_scale_entry" + }, + "caption": { + "$ref": "#/definitions/type_scale_entry" + }, + "cta": { + "$ref": "#/definitions/type_scale_entry" + } + }, + "additionalProperties": { + "$ref": "#/definitions/type_scale_entry" + } + }, + "asset_libraries": { + "type": "array", + "items": { + "$ref": "#/definitions/asset_library" + }, + "description": "References to managed asset libraries (icon sets, illustration systems, image collections). URLs are intended for human access; agent-facing DAM integration is under investigation." + }, + "restrictions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Visual prohibitions and guardrails (e.g., 'Never use black backgrounds', 'Do not crop the logo', 'No stock photography of people on phones')" + } + }, + "additionalProperties": true + }, + "contact": { + "type": "object", + "description": "Contact information", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 255 + }, + "domain": { + "$ref": "#/definitions/domain" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + } + }, + "oneOf": [ + { + "type": "object", + "title": "Authoritative Location Redirect", + "description": "Redirects to a hosted brand.json file at another URL", + "properties": { + "$schema": { + "type": "string" + }, + "authoritative_location": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL of the authoritative brand.json file" + }, + "redirect_reason": { + "type": "string", + "enum": [ + "acquisition", + "divestiture", + "rebrand", + "regional", + "legacy", + "consolidation", + "other" + ], + "description": "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'." + }, + "redirect_effective_at": { + "type": "string", + "format": "date-time", + "description": "Optional timestamp when this redirect became effective. Caches MUST treat any entry cached before this timestamp as stale and re-fetch through the redirect." + }, + "note": { + "type": "string", + "description": "Optional human-readable rationale for the redirect." + }, + "last_updated": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "authoritative_location" + ], + "additionalProperties": false + }, + { + "type": "object", + "title": "House Redirect", + "description": "Redirects to the house domain that contains the full brand portfolio", + "properties": { + "$schema": { + "type": "string" + }, + "house": { + "$ref": "#/definitions/domain", + "description": "House domain to fetch brand portfolio from" + }, + "region": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "ISO 3166-1 alpha-2 country code if this is a regional domain" + }, + "redirect_reason": { + "type": "string", + "enum": [ + "acquisition", + "divestiture", + "rebrand", + "regional", + "legacy", + "consolidation", + "other" + ], + "description": "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'." + }, + "redirect_effective_at": { + "type": "string", + "format": "date-time", + "description": "Optional timestamp when this redirect became effective. Caches MUST treat any entry cached before this timestamp as stale and re-fetch through the redirect." + }, + "note": { + "type": "string", + "description": "Optional human-readable rationale for the redirect." + }, + "last_updated": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "house" + ], + "additionalProperties": false + }, + { + "type": "object", + "title": "Brand Agent", + "description": "Brand represented by agents that provide brand info via MCP", + "properties": { + "$schema": { + "type": "string" + }, + "version": { + "type": "string" + }, + "agents": { + "$ref": "#/definitions/agents" + }, + "brand_agent": { + "$ref": "#/definitions/brand_agent", + "deprecated": true + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "data_subject_contestation": { + "$ref": "#/definitions/data_subject_contestation" + }, + "last_updated": { + "type": "string", + "format": "date-time" + } + }, + "anyOf": [ + { + "required": [ + "agents" + ] + }, + { + "required": [ + "brand_agent" + ] + } + ], + "additionalProperties": false + }, + { + "type": "object", + "title": "House Portfolio", + "description": "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", + "properties": { + "$schema": { + "type": "string" + }, + "version": { + "type": "string" + }, + "house": { + "$ref": "#/definitions/house" + }, + "brands": { + "type": "array", + "description": "Inline brands owned by this house (parent-owned data). Use for sub-brands without their own canonical document \u2014 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[].", + "items": { + "$ref": "#/definitions/brand" + }, + "minItems": 1 + }, + "brand_refs": { + "type": "array", + "description": "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", + "items": { + "$ref": "#/definitions/portfolio_entry" + }, + "minItems": 1 + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "authorized_operators": { + "type": "array", + "description": "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.", + "items": { + "$ref": "#/definitions/authorized_operator" + } + }, + "trademarks": { + "type": "array", + "items": { + "$ref": "#/definitions/trademark" + }, + "description": "House-level (corporate) registered trademarks. Brand-level marks live on individual brand entries; resolution is union." + }, + "last_updated": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "house" + ], + "anyOf": [ + { + "required": [ + "brands" + ] + }, + { + "required": [ + "brand_refs" + ] + } + ], + "additionalProperties": false + }, + { + "type": "object", + "title": "Brand Canonical Document", + "description": "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", + "allOf": [ + { + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "version": { + "type": "string" + }, + "house_domain": { + "$ref": "#/definitions/domain", + "description": "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 \u2014 a brand cannot itself declare brand_refs[]. Omit for standalone brands (no house)." + }, + "last_updated": { + "type": "string", + "format": "date-time" + } + }, + "not": { + "anyOf": [ + { + "required": [ + "house" + ] + }, + { + "required": [ + "brands" + ] + }, + { + "required": [ + "brand_refs" + ] + }, + { + "required": [ + "authorized_operators" + ] + }, + { + "required": [ + "authoritative_location" + ] + }, + { + "required": [ + "redirect_reason" + ] + }, + { + "required": [ + "redirect_effective_at" + ] + }, + { + "required": [ + "region" + ] + }, + { + "required": [ + "note" + ] + } + ] + } + }, + { + "$ref": "#/definitions/brand" + } + ] + } + ], + "examples": [ + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "authoritative_location": "https://adcontextprotocol.org/brand/abc123/brand.json" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "house": "nikeinc.com", + "note": "Redirect to house domain for full brand portfolio" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "agents": [ + { + "type": "brand", + "url": "https://agent.acme.com/mcp", + "id": "acme_brand" + } + ] + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "house": { + "domain": "pg.com", + "name": "Procter & Gamble", + "architecture": "house_of_brands", + "agents": [ + { + "type": "governance", + "url": "https://agents.pg.com/governance", + "id": "pg_governance", + "description": "Brand safety and compliance for all P&G brands", + "jwks_uri": "https://agents.pg.com/.well-known/jwks.json" + } + ] + }, + "brands": [ + { + "id": "tide", + "url": "https://tide.com", + "names": [ + { + "en_US": "Tide" + }, + { + "es_MX": "Tide" + }, + { + "zh_CN": "\u6c70\u6e0d" + } + ], + "keller_type": "master", + "industries": [ + "cpg" + ], + "description": "Laundry detergent brand", + "logos": [ + { + "url": "https://cdn.pg.com/tide/logo-square.png", + "orientation": "square", + "background": "transparent-bg", + "variant": "primary", + "usage": "Primary logo for general use" + }, + { + "url": "https://cdn.pg.com/tide/logo-horizontal-dark.png", + "orientation": "horizontal", + "background": "dark-bg", + "variant": "full-lockup", + "usage": "Full lockup for dark backgrounds" + } + ], + "colors": { + "primary": "#FF6600", + "secondary": "#0066CC", + "background": "#FFFFFF", + "text": "#1A1A1A", + "heading": "#FF6600", + "body": "#333333", + "label": "#666666", + "border": "#E5E5E5", + "divider": "#F0F0F0", + "surface_1": "#F9F9F9", + "surface_2": "#EFEFEF" + }, + "tone": { + "voice": "clean, fresh, trustworthy", + "attributes": [ + "reliable", + "family-friendly", + "confident" + ], + "dos": [ + "Use simple, direct language", + "Emphasize cleaning power" + ], + "donts": [ + "Avoid technical jargon", + "Don't be overly serious" + ] + }, + "tagline": [ + { + "en_US": "Tide's In, Dirt's Out" + } + ], + "visual_guidelines": { + "photography": { + "realism": "natural", + "lighting": "soft daylight", + "color_temperature": "warm", + "contrast": "medium", + "depth_of_field": "medium", + "subject": { + "people": { + "age_range": "25-45", + "diversity": "mixed", + "mood": [ + "confident", + "relaxed", + "happy" + ] + }, + "product_focus": "in-use", + "setting": "indoor" + }, + "framing": { + "subject_position": "center", + "crop_style": "waist-up", + "perspective": "eye-level" + }, + "preferred_aspect_ratios": [ + "16:9", + "4:5", + "1:1" + ] + }, + "graphic_style": { + "style_type": "flat_illustration", + "stroke_style": "rounded", + "stroke_weight": "2px", + "corner_radius": "12px" + }, + "shapes": { + "primary_shape": "circle", + "secondary_shapes": [ + "rounded_rectangle" + ], + "usage": { + "max_per_layout": 2, + "overlap_allowed": false + } + }, + "iconography": { + "style": "outline", + "stroke_weight": "2px", + "corner_style": "rounded", + "usage": { + "max_per_frame": 3, + "size_ratio": "1:8" + } + }, + "composition": { + "overlays": { + "gradient_style": "linear", + "gradient_direction": "180deg", + "opacity": "60%" + }, + "texture": { + "style": "none" + }, + "backgrounds": { + "types_allowed": [ + "solid_color", + "gradient", + "blurred_photo" + ] + } + }, + "border_radius": { + "none": "0", + "default": "12px", + "small": "4px", + "large": "20px", + "pill": "999px" + }, + "elevation": { + "none": "none", + "subtle": "0 1px 3px rgba(0,0,0,0.08)", + "card": "0 4px 8px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.06)", + "modal": "0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.06)" + }, + "spacing": { + "unit": "8px", + "scale": { + "xs": "4px", + "sm": "8px", + "md": "16px", + "lg": "24px", + "xl": "32px", + "2xl": "48px" + } + }, + "motion": { + "transition_style": "dissolve", + "animation_speed": "moderate", + "easing": "ease-in-out", + "text_entrance": "fade", + "pacing": "moderate", + "kinetic_typography": false + }, + "logo_placement": { + "preferred_position": "bottom-right", + "min_clear_space": "0.5x", + "min_height": "32px", + "background_contrast": "any" + }, + "colorways": [ + { + "name": "primary", + "foreground": "#FFFFFF", + "background": "#FF6600", + "accent": "#0066CC" + }, + { + "name": "inverted", + "foreground": "#FF6600", + "background": "#FFFFFF", + "accent": "#0066CC", + "border": "#FF6600" + } + ], + "type_scale": { + "base_width": "1080px", + "heading": { + "font": "primary", + "size": "48px", + "weight": "700", + "line_height": "1.1", + "text_transform": "none" + }, + "subheading": { + "font": "primary", + "size": "24px", + "weight": "600", + "line_height": "1.3" + }, + "body": { + "font": "secondary", + "size": "16px", + "weight": "400", + "line_height": "1.5" + }, + "cta": { + "font": "primary", + "size": "18px", + "weight": "700", + "text_transform": "uppercase", + "letter_spacing": "0.05em" + } + }, + "restrictions": [ + "Never place text over the product", + "Do not use black backgrounds", + "No stock photography of people on phones" + ] + }, + "properties": [ + { + "type": "website", + "identifier": "tide.com", + "primary": true + }, + { + "type": "mobile_app", + "store": "apple", + "identifier": "com.pg.tide" + } + ], + "contact": { + "email": "brands@pg.com" + } + }, + { + "id": "pampers", + "url": "https://pampers.com", + "names": [ + { + "en_US": "Pampers" + } + ], + "keller_type": "master", + "industries": [ + "cpg" + ], + "logos": [ + { + "url": "https://cdn.pg.com/pampers/logo.png", + "orientation": "horizontal", + "variant": "primary" + } + ], + "colors": { + "primary": "#00A0D2" + }, + "properties": [ + { + "type": "website", + "identifier": "pampers.com", + "primary": true + } + ] + } + ], + "contact": { + "name": "P&G Brand Team", + "email": "brands@pg.com" + }, + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "house": { + "domain": "nikeinc.com", + "name": "Nike, Inc.", + "architecture": "hybrid" + }, + "brands": [ + { + "id": "nike", + "url": "https://nike.com", + "names": [ + { + "en_US": "Nike" + }, + { + "zh_CN": "\u8010\u514b" + }, + { + "ja_JP": "\u30ca\u30a4\u30ad" + } + ], + "keller_type": "master", + "logos": [ + { + "url": "https://cdn.nike.com/swoosh-dark.svg", + "orientation": "horizontal", + "background": "dark-bg", + "variant": "icon", + "usage": "Swoosh icon for dark backgrounds" + }, + { + "url": "https://cdn.nike.com/logo-full.svg", + "orientation": "horizontal", + "background": "light-bg", + "variant": "full-lockup", + "usage": "Full logo with wordmark for light backgrounds" + } + ], + "colors": { + "primary": "#111111", + "accent": "#FF6600" + }, + "tone": "inspirational, bold, athletic", + "tagline": [ + { + "en_US": "Just Do It" + } + ], + "properties": [ + { + "type": "website", + "identifier": "nike.com", + "primary": true + }, + { + "type": "website", + "identifier": "nike.cn", + "region": "CN" + }, + { + "type": "mobile_app", + "store": "apple", + "identifier": "com.nike.omega" + } + ] + }, + { + "id": "air_jordan", + "url": "https://jordan.com", + "names": [ + { + "en_US": "Air Jordan" + }, + { + "en_US": "Jordan" + }, + { + "en_US": "Jumpman" + } + ], + "keller_type": "endorsed", + "parent_brand": "nike", + "logos": [ + { + "url": "https://cdn.nike.com/jumpman.svg", + "orientation": "square", + "background": "transparent-bg", + "variant": "icon" + } + ], + "colors": { + "primary": "#CE1141", + "secondary": "#111111" + }, + "properties": [ + { + "type": "website", + "identifier": "jordan.com", + "primary": true + }, + { + "type": "website", + "identifier": "jumpman23.com" + } + ], + "brand_agent": { + "url": "https://dam.nike.com/mcp", + "id": "nike_dam" + } + }, + { + "id": "converse", + "url": "https://converse.com", + "names": [ + { + "en_US": "Converse" + } + ], + "keller_type": "independent", + "logos": [ + { + "url": "https://cdn.converse.com/star.svg", + "orientation": "square", + "variant": "icon" + } + ], + "properties": [ + { + "type": "website", + "identifier": "converse.com", + "primary": true + } + ] + } + ], + "authorized_operators": [ + { + "domain": "wpp.com", + "brands": [ + "nike", + "air_jordan" + ], + "countries": [ + "US", + "GB", + "DE", + "FR" + ] + }, + { + "domain": "dentsu.co.jp", + "brands": [ + "nike" + ], + "countries": [ + "JP" + ] + }, + { + "domain": "nike.com", + "brands": [ + "*" + ] + } + ], + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "house": { + "domain": "mediavine.com", + "name": "Mediavine", + "architecture": "branded_house" + }, + "brands": [ + { + "id": "mediavine", + "url": "https://mediavine.com", + "names": [ + { + "en_US": "Mediavine" + } + ], + "keller_type": "master", + "properties": [ + { + "type": "website", + "identifier": "mediavine.com", + "primary": true + }, + { + "type": "website", + "identifier": "thehollywoodgossip.com", + "relationship": "delegated" + }, + { + "type": "website", + "identifier": "foodfanatic.com", + "relationship": "delegated" + }, + { + "type": "website", + "identifier": "thebiglead.com", + "relationship": "delegated" + } + ], + "agents": [ + { + "type": "sales", + "url": "https://ads.mediavine.com/mcp", + "id": "mediavine_sales" + } + ] + } + ], + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "house": { + "domain": "nikeinc.com", + "name": "Nike, Inc.", + "architecture": "hybrid" + }, + "brands": [ + { + "id": "nike_sb", + "names": [ + { + "en_US": "Nike SB" + } + ], + "keller_type": "sub_brand", + "logos": [ + { + "url": "https://nike.com/sb/logo.svg", + "variant": "primary" + } + ] + } + ], + "brand_refs": [ + { + "domain": "converse.com", + "brand_id": "converse" + }, + { + "domain": "jordan.com", + "brand_id": "jordan" + } + ], + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "house": { + "domain": "wpp.com", + "name": "WPP plc" + }, + "brand_refs": [ + { + "domain": "bbh-sport.com", + "brand_id": "bbh_sport", + "managed_by": "bbh.com" + }, + { + "domain": "ogilvy-toyota.com", + "brand_id": "ogilvy_toyota", + "managed_by": "ogilvy.com" + }, + { + "domain": "wpp-direct.com", + "brand_id": "wpp_direct" + } + ], + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "id": "converse", + "names": [ + { + "en_US": "Converse" + } + ], + "keller_type": "sub_brand", + "house_domain": "nikeinc.com", + "logos": [ + { + "url": "https://converse.com/logo.svg", + "variant": "primary" + } + ], + "tagline": "Sneaker for the streets", + "last_updated": "2026-01-15T10:00:00Z" + }, + { + "$schema": "/schemas/3.1.0-beta.5/brand.json", + "version": "1.0", + "id": "patagonia", + "names": [ + { + "en_US": "Patagonia" + } + ], + "keller_type": "independent", + "logos": [ + { + "url": "https://patagonia.com/logo.svg", + "variant": "primary" + } + ], + "tagline": "We're in business to save our home planet.", + "last_updated": "2026-01-15T10:00:00Z" + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/acquire-rights-request.json b/schemas/cache/3.1.0-beta.5/brand/acquire-rights-request.json new file mode 100644 index 000000000..f1e8506ca --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/acquire-rights-request.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Acquire Rights Request", + "description": "Binding contractual request to acquire rights from a brand agent. Parallels create_media_buy \u2014 the buyer selects a pricing_option_id from a get_rights response and provides campaign details. The agent clears against existing contracts and returns terms, generation credentials, and disclosure requirements.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-status": "experimental", + "x-mutates-state": true, + "properties": { + "rights_id": { + "type": "string", + "description": "Rights offering identifier from get_rights response", + "x-entity": "rights_grant" + }, + "pricing_option_id": { + "type": "string", + "description": "Selected pricing option from the rights offering", + "x-entity": "vendor_pricing_option" + }, + "buyer": { + "$ref": "../core/brand-ref.json", + "description": "The buyer's brand identity" + }, + "account": { + "$ref": "../core/account-ref.json", + "description": "Account context for this acquisition. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance. When both an inline governance_context token (on the protocol envelope) and a bound governance agent are present, the inline token wins \u2014 brand agents MUST consult the agent identified by the inline token. When the request omits both `account` and an inline governance_context token, the brand agent treats the acquisition as ungoverned and the CPM-projection rule on `campaign.estimated_impressions` does not apply (sellers MAY refuse to transact ungoverned requests as a matter of commercial policy). Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts." + }, + "campaign": { + "type": "object", + "description": "Campaign details for rights clearance", + "properties": { + "description": { + "type": "string", + "description": "Description of how the rights will be used" + }, + "uses": { + "type": "array", + "description": "Specific rights uses for this campaign", + "items": { + "$ref": "../enums/right-use.json" + }, + "minItems": 1 + }, + "countries": { + "type": "array", + "description": "Countries where the campaign will run (ISO 3166-1 alpha-2)", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "format_ids": { + "type": "array", + "description": "Creative formats that will be produced", + "items": { + "$ref": "../core/format-id.json" + } + }, + "estimated_impressions": { + "type": "integer", + "minimum": 0, + "description": "Estimated total impressions for the campaign. Required when the brand agent will project commitment against a governance plan AND the selected pricing_option has model: 'cpm' \u2014 projection equals (pricing_option.price / 1000) \u00d7 estimated_impressions evaluated in pricing_option.currency. The brand agent will project commitment whenever the request is governance-aware via either (a) an intent-phase governance_context token on the protocol envelope, or (b) `account` resolving to an account that has a governance agent previously bound via sync_governance. Brand agents MUST reject with INVALID_REQUEST (field: campaign.estimated_impressions) in either path when CPM-priced rights are requested and this field is omitted or zero; implementer-chosen defaults are non-conformant. See the acquire_rights task reference for the full validation contract including currency-mismatch handling." + }, + "start_date": { + "type": "string", + "format": "date", + "description": "Campaign start date (ISO 8601)" + }, + "end_date": { + "type": "string", + "format": "date", + "description": "Campaign end date (ISO 8601). Brand agents MUST reject with INVALID_REQUEST (field: campaign.end_date) when end_date is in the past at the time of the request \u2014 acquiring rights for an elapsed window produces a zero-duration grant and is almost always a buyer-side bug." + } + }, + "required": [ + "description", + "uses" + ], + "additionalProperties": true + }, + "revocation_webhook": { + "$ref": "../core/push-notification-config.json", + "description": "Webhook for rights revocation notifications. If the rights holder needs to revoke rights (talent scandal, contract violation, etc.), they POST a revocation-notification to this URL. The buyer is responsible for stopping creative delivery upon receipt." + }, + "push_notification_config": { + "$ref": "../core/push-notification-config.json", + "description": "Webhook for async status updates if the acquisition requires approval. The rights agent sends a webhook notification when the status transitions to acquired or rejected." + }, + "idempotency_key": { + "type": "string", + "description": "Client-generated key for safe retries. Resubmitting with the same key returns the original response rather than creating a duplicate acquisition. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "rights_id", + "pricing_option_id", + "buyer", + "campaign", + "revocation_webhook" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/acquire-rights-response.json b/schemas/cache/3.1.0-beta.5/brand/acquire-rights-response.json new file mode 100644 index 000000000..ea6e7171a --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/acquire-rights-response.json @@ -0,0 +1,228 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Acquire Rights Response", + "description": "Result of a rights acquisition request. Returns one of three rights_status values: acquired (with terms and generation credentials), pending_approval (requires rights holder review), or rejected (with reason). Uses discriminated union on `rights_status` field.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "x-status": "experimental", + "oneOf": [ + { + "title": "AcquireRightsAcquired", + "properties": { + "rights_id": { + "type": "string", + "description": "Rights grant identifier", + "x-entity": "rights_grant" + }, + "rights_status": { + "type": "string", + "const": "acquired", + "description": "Rights have been cleared and credentials issued. Renamed from `status` in 3.1 to free the top-level `status` key for the envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization (#4878)." + }, + "brand_id": { + "type": "string", + "description": "Brand identifier of the rights subject", + "x-entity": "rights_holder_brand" + }, + "terms": { + "$ref": "rights-terms.json", + "description": "Agreed contractual terms" + }, + "generation_credentials": { + "type": "array", + "description": "Scoped credentials for generating rights-cleared content", + "items": { + "$ref": "../core/generation-credential.json" + } + }, + "restrictions": { + "type": "array", + "description": "Usage restrictions and requirements", + "items": { + "type": "string" + } + }, + "disclosure": { + "type": "object", + "description": "Required disclosure for creatives using these rights", + "properties": { + "required": { + "type": "boolean", + "description": "Whether disclosure is required" + }, + "text": { + "type": "string", + "description": "Disclosure text to include with the creative" + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "approval_webhook": { + "$ref": "../core/push-notification-config.json", + "description": "Authenticated webhook for submitting creatives for approval. POST a creative-approval-request to the URL using the provided authentication. The response is a creative-approval-response." + }, + "usage_reporting_url": { + "type": "string", + "format": "uri", + "description": "Endpoint for reporting usage against these rights" + }, + "rights_constraint": { + "$ref": "../core/rights-constraint.json", + "description": "Pre-built rights constraint for embedding in creative manifests. Populated from the agreed terms \u2014 the buyer does not need to construct it manually." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "rights_id", + "rights_status", + "brand_id", + "terms", + "generation_credentials", + "rights_constraint" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "AcquireRightsPendingApproval", + "properties": { + "rights_id": { + "type": "string", + "x-entity": "rights_grant" + }, + "rights_status": { + "type": "string", + "const": "pending_approval", + "description": "Rights require approval from the rights holder" + }, + "brand_id": { + "type": "string", + "x-entity": "rights_holder_brand" + }, + "detail": { + "type": "string", + "description": "Explanation of what requires approval" + }, + "estimated_response_time": { + "type": "string", + "description": "Expected time for approval decision (e.g., '48h', '3 business days')" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "rights_id", + "rights_status", + "brand_id" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "AcquireRightsRejected", + "properties": { + "rights_id": { + "type": "string", + "x-entity": "rights_grant" + }, + "rights_status": { + "type": "string", + "const": "rejected", + "description": "Rights request was rejected" + }, + "brand_id": { + "type": "string", + "x-entity": "rights_holder_brand" + }, + "reason": { + "type": "string", + "description": "Why the rights request was rejected. May be sanitized to protect confidential brand rules \u2014 e.g., 'This violates our public figures brand guidelines' rather than naming the specific rule." + }, + "suggestions": { + "type": "array", + "description": "Actionable alternatives the buyer can try. If present, the rejection is fixable \u2014 the buyer can adjust their request. If absent, the rejection is final for this talent/rights combination.", + "items": { + "type": "string" + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "rights_id", + "rights_status", + "brand_id", + "reason" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "AcquireRightsError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "rights_status" + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/creative-approval-request.json b/schemas/cache/3.1.0-beta.5/brand/creative-approval-request.json new file mode 100644 index 000000000..cb3b400b5 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/creative-approval-request.json @@ -0,0 +1,61 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Creative Approval Request", + "description": "Payload submitted by the buyer to the approval_webhook URL from acquire_rights. Contains the creative for rights holder review before distribution.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-mutates-state": true, + "properties": { + "rights_id": { + "type": "string", + "description": "Rights grant this creative was produced under", + "x-entity": "rights_grant" + }, + "creative_id": { + "type": "string", + "description": "Buyer-assigned creative identifier. Equivalent to OpenRTB crid. Used to track approval status across resubmissions.", + "x-entity": "creative" + }, + "creative_url": { + "type": "string", + "format": "uri", + "description": "URL where the creative asset can be retrieved for review" + }, + "creative_format": { + "$ref": "../core/format-id.json", + "description": "Format of the creative being submitted" + }, + "description": { + "type": "string", + "description": "Description of the creative for reviewer context" + }, + "metadata": { + "type": "object", + "description": "Additional creative metadata (duration, dimensions, target audience, etc.)", + "additionalProperties": true + }, + "idempotency_key": { + "type": "string", + "description": "Client-generated key for safe retries. Resubmitting with the same key returns the original response. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "rights_id", + "creative_url" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/creative-approval-response.json b/schemas/cache/3.1.0-beta.5/brand/creative-approval-response.json new file mode 100644 index 000000000..280d995bf --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/creative-approval-response.json @@ -0,0 +1,197 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Creative Approval Response", + "description": "Response from the approval_webhook after reviewing a submitted creative. Uses discriminated union on `approval_status` field: approved, rejected, or pending_review.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "CreativeApproved", + "properties": { + "approval_status": { + "type": "string", + "const": "approved", + "description": "Creative has been approved for distribution. Renamed from `status` in 3.1 to free the top-level `status` key for the envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization (#4878)." + }, + "rights_id": { + "type": "string", + "x-entity": "rights_grant" + }, + "creative_id": { + "type": "string", + "description": "Echo of the buyer's creative identifier", + "x-entity": "creative" + }, + "creative_url": { + "type": "string", + "format": "uri" + }, + "approved_at": { + "type": "string", + "format": "date-time" + }, + "conditions": { + "type": "array", + "description": "Conditions on the approval (e.g., 'approved for NL market only')", + "items": { + "type": "string" + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "approval_status", + "rights_id" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "CreativeRejected", + "properties": { + "approval_status": { + "type": "string", + "const": "rejected", + "description": "Creative was rejected" + }, + "rights_id": { + "type": "string", + "x-entity": "rights_grant" + }, + "creative_id": { + "type": "string", + "description": "Echo of the buyer's creative identifier", + "x-entity": "creative" + }, + "creative_url": { + "type": "string", + "format": "uri" + }, + "reason": { + "type": "string", + "description": "Why the creative was rejected" + }, + "suggestions": { + "type": "array", + "description": "Actionable feedback for revision. If present, the buyer can revise and resubmit the creative. If absent, the rejection is final for this creative concept.", + "items": { + "type": "string" + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "approval_status", + "rights_id", + "reason" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "CreativePendingReview", + "properties": { + "approval_status": { + "type": "string", + "const": "pending_review", + "description": "Creative is queued for review" + }, + "rights_id": { + "type": "string", + "x-entity": "rights_grant" + }, + "creative_id": { + "type": "string", + "description": "Echo of the buyer's creative identifier", + "x-entity": "creative" + }, + "creative_url": { + "type": "string", + "format": "uri" + }, + "estimated_response_time": { + "type": "string", + "description": "Expected time for review (e.g., '24h', '2 business days')" + }, + "status_url": { + "type": "string", + "format": "uri", + "description": "URL to poll for updated approval status. GET this URL to receive a creative-approval-response. Poll at reasonable intervals (suggested: every 5 minutes, back off after 1 hour to every 30 minutes). Stop polling after estimated_response_time has elapsed and the approval_status is still pending_review." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "approval_status", + "rights_id" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "CreativeApprovalError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "approval_status" + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/get-brand-identity-request.json b/schemas/cache/3.1.0-beta.5/brand/get-brand-identity-request.json new file mode 100644 index 000000000..3d03138be --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/get-brand-identity-request.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Brand Identity Request", + "description": "Request brand identity data from a brand agent. Core identity (house, names, description, logos) is always public. Linked accounts get deeper data: high-res assets, voice configs, tone guidelines, and rights availability.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier from brand.json brands array", + "x-entity": "advertiser_brand" + }, + "fields": { + "type": "array", + "description": "Optional identity sections to include in the response. When omitted, all sections the caller is authorized to see are returned. Core fields (brand_id, house, names) are always returned and do not need to be requested.", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "description", + "industries", + "keller_type", + "logos", + "colors", + "fonts", + "visual_guidelines", + "tone", + "tagline", + "voice_synthesis", + "assets", + "rights" + ] + } + }, + "use_case": { + "type": "string", + "description": "Intended use case, so the agent can tailor the response. A 'voice_synthesis' use case returns voice configs; a 'likeness' use case returns high-res photos and appearance guidelines." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "brand_id" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/get-brand-identity-response.json b/schemas/cache/3.1.0-beta.5/brand/get-brand-identity-response.json new file mode 100644 index 000000000..656176401 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/get-brand-identity-response.json @@ -0,0 +1,593 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Brand Identity Response", + "description": "Brand identity data from a brand agent. Core identity (house, names, description, logos) is always public. Authorized callers receive richer data (high-res assets, voice synthesis, tone guidelines, rights availability). Includes available_fields to signal what the caller could unlock by linking their account.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "GetBrandIdentitySuccess", + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier", + "x-entity": "advertiser_brand" + }, + "house": { + "type": "object", + "description": "The house (corporate entity) this brand belongs to. Always returned regardless of authorization level.", + "properties": { + "domain": { + "type": "string", + "description": "House domain (e.g., nikeinc.com)" + }, + "name": { + "type": "string", + "description": "House display name" + } + }, + "required": [ + "domain", + "name" + ], + "additionalProperties": true + }, + "names": { + "type": "array", + "description": "Localized brand names with BCP 47 locale code keys (e.g., 'en_US', 'fr_CA'). Bare language codes ('en') are accepted as wildcards for backwards compatibility.", + "items": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "string" + } + } + }, + "description": { + "type": "string", + "description": "Brand description" + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Brand industries." + }, + "keller_type": { + "type": "string", + "enum": [ + "master", + "sub_brand", + "endorsed", + "independent" + ], + "description": "Brand architecture type: master (primary brand of house), sub_brand (carries parent name), endorsed (independent identity backed by parent), independent (operates separately)" + }, + "logos": { + "type": "array", + "description": "Brand logos. Public callers get standard logos; authorized callers also receive high-res variants. Shape matches brand.json logo definition.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL to the logo asset" + }, + "orientation": { + "type": "string", + "enum": [ + "square", + "horizontal", + "vertical", + "stacked" + ], + "description": "Logo aspect ratio orientation" + }, + "background": { + "type": "string", + "enum": [ + "dark-bg", + "light-bg", + "transparent-bg" + ], + "description": "Background compatibility" + }, + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary", + "icon", + "wordmark", + "full-lockup" + ], + "description": "Logo variant type" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional semantic tags" + }, + "usage": { + "type": "string", + "description": "When to use this logo variant" + }, + "width": { + "type": "integer", + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "description": "Height in pixels" + } + }, + "required": [ + "url" + ], + "additionalProperties": true + } + }, + "colors": { + "type": "object", + "description": "Brand color palette. Each role accepts a single hex color or an array of hex colors. Shape matches brand.json colors definition.", + "properties": { + "primary": { + "oneOf": [ + { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "minItems": 1 + } + ] + }, + "secondary": { + "oneOf": [ + { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "minItems": 1 + } + ] + }, + "accent": { + "oneOf": [ + { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "minItems": 1 + } + ] + }, + "background": { + "oneOf": [ + { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "minItems": 1 + } + ] + }, + "text": { + "oneOf": [ + { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "minItems": 1 + } + ] + } + }, + "additionalProperties": true + }, + "fonts": { + "type": "object", + "description": "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 family name and font files. Shape matches brand.json fonts definition.", + "maxProperties": 20, + "definitions": { + "font_role": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "family": { + "type": "string", + "description": "CSS font-family name" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL to the font file" + }, + "weight": { + "type": "integer", + "minimum": 100, + "maximum": 900, + "description": "CSS numeric font-weight" + }, + "weight_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 100, + "maximum": 900 + }, + "minItems": 2, + "maxItems": 2, + "description": "Variable font weight axis range as [min, max]" + }, + "style": { + "type": "string", + "enum": [ + "normal", + "italic", + "oblique" + ], + "description": "CSS font-style" + } + }, + "required": [ + "url" + ], + "additionalProperties": true + }, + "maxItems": 36 + }, + "opentype_features": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]{4}$" + }, + "maxItems": 20, + "description": "OpenType feature tags to enable (e.g., ['ss01', 'tnum'])" + }, + "fallbacks": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "maxItems": 10, + "description": "Ordered fallback font-family names for script coverage" + } + }, + "required": [ + "family" + ], + "additionalProperties": true + } + ] + } + }, + "properties": { + "primary": { + "$ref": "#/oneOf/0/properties/fonts/definitions/font_role", + "description": "Primary font family" + }, + "secondary": { + "$ref": "#/oneOf/0/properties/fonts/definitions/font_role", + "description": "Secondary font family" + } + }, + "additionalProperties": { + "$ref": "#/oneOf/0/properties/fonts/definitions/font_role" + } + }, + "visual_guidelines": { + "type": "object", + "additionalProperties": true, + "description": "Structured visual rules for generative creative systems (photography, graphic_style, colorways, type_scale, motion). Matches brand.json visual_guidelines definition. Authorized callers only." + }, + "tone": { + "type": "object", + "description": "Brand voice and messaging guidelines", + "properties": { + "voice": { + "type": "string", + "description": "Brand personality described as comma-separated adjectives (e.g., 'enthusiastic, warm, competitive')" + }, + "attributes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Personality traits that characterize the brand voice, used as prompt guidance" + }, + "dos": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Approved messaging approaches, content themes, and reference points" + }, + "donts": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Prohibited topics, competitor references, and phrasings to avoid" + } + }, + "additionalProperties": true + }, + "tagline": { + "oneOf": [ + { + "type": "string", + "description": "Plain tagline string for backwards compatibility" + }, + { + "type": "array", + "description": "Localized taglines with BCP 47 locale codes", + "items": { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "minItems": 1 + } + ], + "description": "Brand tagline or slogan. Accepts a plain string or a localized array matching the names pattern." + }, + "voice_synthesis": { + "type": "object", + "description": "Voice synthesis configuration for AI-generated audio", + "properties": { + "provider": { + "type": "string" + }, + "voice_id": { + "type": "string" + }, + "settings": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "assets": { + "type": "array", + "description": "Available brand assets (images, audio, video). Authorized callers only. Shape matches brand.json asset definition.", + "items": { + "type": "object", + "properties": { + "asset_id": { + "type": "string", + "description": "Unique identifier" + }, + "asset_type": { + "$ref": "../enums/asset-content-type.json", + "description": "Type of asset content" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to CDN-hosted asset file" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags for discovery" + }, + "name": { + "type": "string", + "description": "Human-readable name" + }, + "description": { + "type": "string", + "description": "Asset description or usage notes" + }, + "width": { + "type": "integer", + "description": "Image/video width in pixels" + }, + "height": { + "type": "integer", + "description": "Image/video height in pixels" + }, + "duration_seconds": { + "type": "number", + "description": "Video/audio duration in seconds" + }, + "file_size_bytes": { + "type": "integer", + "description": "File size in bytes" + }, + "format": { + "type": "string", + "description": "File format (e.g., 'jpg', 'mp4')" + } + }, + "required": [ + "asset_id", + "asset_type", + "url" + ], + "additionalProperties": true + } + }, + "rights": { + "type": "object", + "description": "Rights availability summary. For detailed pricing, use get_rights.", + "properties": { + "available_uses": { + "type": "array", + "items": { + "$ref": "../enums/right-use.json" + } + }, + "countries": { + "type": "array", + "description": "Countries where rights are available (ISO 3166-1 alpha-2). If omitted, rights are available worldwide.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "excluded_countries": { + "type": "array", + "description": "Countries excluded from availability (ISO 3166-1 alpha-2)", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "exclusivity_model": { + "type": "string" + }, + "content_restrictions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true + }, + "available_fields": { + "type": "array", + "description": "Fields available but not returned in this response due to authorization level. Tells the caller what they would gain by linking their account via sync_accounts. Values match the request fields enum.", + "items": { + "type": "string", + "enum": [ + "description", + "industries", + "keller_type", + "logos", + "colors", + "fonts", + "visual_guidelines", + "tone", + "tagline", + "voice_synthesis", + "assets", + "rights" + ] + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "brand_id", + "house", + "names" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "GetBrandIdentityError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "brand_id" + ] + }, + { + "required": [ + "house" + ] + }, + { + "required": [ + "names" + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/get-rights-request.json b/schemas/cache/3.1.0-beta.5/brand/get-rights-request.json new file mode 100644 index 000000000..cbe3b030e --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/get-rights-request.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Rights Request", + "description": "Search for licensable rights across a brand agent's roster. Returns matches with pricing. Discovery is natural-language-first \u2014 no taxonomy for categories. The agent interprets intent from the query and filters based on the buyer's brand compatibility.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-status": "experimental", + "properties": { + "query": { + "type": "string", + "description": "Natural language description of desired rights. The agent interprets intent, budget signals, and compatibility from this text.", + "maxLength": 2000 + }, + "uses": { + "type": "array", + "description": "Rights uses being requested. The agent returns options covering these uses, potentially bundled into composite pricing.", + "items": { + "$ref": "../enums/right-use.json" + }, + "minItems": 1 + }, + "buyer_brand": { + "$ref": "../core/brand-ref.json", + "description": "The buyer's brand. The agent fetches the buyer's brand.json for compatibility filtering (e.g., dietary conflicts, competitor exclusions)." + }, + "countries": { + "type": "array", + "description": "Countries where rights are needed (ISO 3166-1 alpha-2). Filters to rights available in these markets.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "brand_id": { + "type": "string", + "description": "Search within a specific brand's rights. If omitted, searches across the agent's full roster.", + "x-entity": "rights_holder_brand" + }, + "right_type": { + "$ref": "../enums/right-type.json", + "description": "Filter by type of rights (talent, music, stock_media, etc.)" + }, + "include_excluded": { + "type": "boolean", + "description": "Include filtered-out results in the excluded array with reasons. Defaults to false.", + "default": false + }, + "pagination": { + "$ref": "../core/pagination-request.json", + "description": "Pagination parameters for large result sets" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "query", + "uses" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/get-rights-response.json b/schemas/cache/3.1.0-beta.5/brand/get-rights-response.json new file mode 100644 index 000000000..0c31f2d60 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/get-rights-response.json @@ -0,0 +1,228 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Rights Response", + "description": "Licensable rights matching the search criteria, with pricing options. Each result is a complete snapshot of current availability (stateless, DDEX PIE pattern). Excluded results explain why they were filtered out.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "x-status": "experimental", + "oneOf": [ + { + "title": "GetRightsSuccess", + "properties": { + "rights": { + "type": "array", + "description": "Matching rights with pricing options, ranked by relevance", + "items": { + "type": "object", + "properties": { + "rights_id": { + "type": "string", + "description": "Identifier for this rights offering. Referenced in acquire_rights.", + "x-entity": "rights_grant" + }, + "brand_id": { + "type": "string", + "description": "Brand identifier from the agent's roster", + "x-entity": "rights_holder_brand" + }, + "name": { + "type": "string", + "description": "Display name of the rights subject" + }, + "description": { + "type": "string", + "description": "Description of the rights subject" + }, + "right_type": { + "$ref": "../enums/right-type.json" + }, + "match_score": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Relevance score from 0 to 1" + }, + "match_reasons": { + "type": "array", + "description": "Human-readable reasons for the match", + "items": { + "type": "string" + } + }, + "available_uses": { + "type": "array", + "description": "Rights uses available for licensing", + "items": { + "$ref": "../enums/right-use.json" + } + }, + "countries": { + "type": "array", + "description": "Countries where rights are available (ISO 3166-1 alpha-2). When both countries and excluded_countries are present, the effective set is countries minus excluded_countries. If neither is present, all countries are available.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "excluded_countries": { + "type": "array", + "description": "Countries excluded from availability", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "exclusivity_status": { + "type": "object", + "description": "Current exclusivity availability", + "properties": { + "available": { + "type": "boolean", + "description": "Whether exclusivity is available" + }, + "existing_exclusives": { + "type": "array", + "description": "Active exclusivity commitments that may affect availability. Implementers should use vague descriptions ('exclusive commitment in this category') rather than specific deal terms to protect confidential business relationships.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true + }, + "pricing_options": { + "type": "array", + "description": "Available pricing options for these rights", + "items": { + "$ref": "rights-pricing-option.json" + }, + "minItems": 1 + }, + "content_restrictions": { + "type": "array", + "description": "Content restrictions or approval requirements", + "items": { + "type": "string" + } + }, + "preview_assets": { + "type": "array", + "description": "Preview-only assets for evaluation", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "usage": { + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": true + } + } + }, + "required": [ + "rights_id", + "brand_id", + "name", + "available_uses", + "pricing_options" + ], + "additionalProperties": true + } + }, + "excluded": { + "type": "array", + "description": "Results that matched but were filtered out, with reasons", + "items": { + "type": "object", + "properties": { + "brand_id": { + "type": "string", + "x-entity": "rights_holder_brand" + }, + "name": { + "type": "string" + }, + "reason": { + "type": "string", + "description": "Why this result was excluded. May be sanitized to protect confidential brand rules." + }, + "suggestions": { + "type": "array", + "description": "Actionable alternatives if the exclusion is fixable (e.g., 'Available in BE and DE markets'). Absent if the exclusion is final.", + "items": { + "type": "string" + } + } + }, + "required": [ + "brand_id", + "reason" + ], + "additionalProperties": true + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "rights" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "GetRightsError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "rights" + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/revocation-notification.json b/schemas/cache/3.1.0-beta.5/brand/revocation-notification.json new file mode 100644 index 000000000..04935114b --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/revocation-notification.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Revocation Notification", + "description": "Payload sent by a rights holder to a buyer's revocation_webhook when rights are revoked. The buyer must cease creative delivery by effective_at. Partial revocation is supported \u2014 if revoked_uses is present, only those uses are revoked.", + "type": "object", + "properties": { + "idempotency_key": { + "type": "string", + "description": "Sender-generated key stable across retries of the same revocation notification. Rights holders MUST generate a cryptographically random value (UUID v4 recommended) per distinct revocation event and reuse the same key when retrying delivery. Buyers MUST dedupe by this key, scoped to the authenticated sender identity (HMAC secret or Bearer credential); keys from different senders are independent.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "rights_id": { + "type": "string", + "description": "The revoked rights grant identifier", + "x-entity": "rights_grant" + }, + "brand_id": { + "type": "string", + "description": "Brand identifier of the rights subject", + "x-entity": "rights_holder_brand" + }, + "reason": { + "type": "string", + "description": "Human-readable reason for revocation" + }, + "effective_at": { + "type": "string", + "format": "date-time", + "description": "When the revocation takes effect. Immediate revocations use current time. Grace periods use a future time. The buyer must stop serving creative using these rights by this time." + }, + "revoked_uses": { + "type": "array", + "description": "If present, only these uses are revoked (partial revocation). If absent, all uses under the grant are revoked.", + "items": { + "$ref": "../enums/right-use.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "rights_id", + "brand_id", + "reason", + "effective_at" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/rights-pricing-option.json b/schemas/cache/3.1.0-beta.5/brand/rights-pricing-option.json new file mode 100644 index 000000000..72e0f6135 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/rights-pricing-option.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Rights Pricing Option", + "description": "A pricing option for licensable rights. Separate from media-buy pricing options \u2014 rights pricing includes period, impression caps, overage rates, and use-type scoping.", + "x-status": "experimental", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option. Referenced in acquire_rights and report_usage." + }, + "model": { + "$ref": "../enums/pricing-model.json", + "description": "Pricing model (cpm, flat_rate, etc.)" + }, + "price": { + "type": "number", + "minimum": 0, + "description": "Price amount. Interpretation depends on model: CPM = cost per 1,000 impressions, flat_rate = fixed cost per period." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code" + }, + "uses": { + "type": "array", + "description": "Which rights uses this pricing option covers. A single option can bundle multiple uses (e.g., likeness + voice).", + "items": { + "$ref": "../enums/right-use.json" + }, + "minItems": 1 + }, + "period": { + "$ref": "../enums/rights-billing-period.json", + "description": "Billing period for flat_rate and time-based models" + }, + "impression_cap": { + "type": "integer", + "minimum": 1, + "description": "Maximum impressions included in this pricing option per period" + }, + "overage_cpm": { + "type": "number", + "minimum": 0, + "description": "CPM rate applied to impressions exceeding the impression_cap" + }, + "description": { + "type": "string", + "description": "Human-readable description of this pricing option" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "pricing_option_id", + "model", + "price", + "currency", + "uses" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/rights-terms.json b/schemas/cache/3.1.0-beta.5/brand/rights-terms.json new file mode 100644 index 000000000..d1f37174d --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/rights-terms.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Rights Terms", + "description": "Contractual terms for a rights grant. Shared between acquire_rights and update_rights responses.", + "x-status": "experimental", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string" + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "period": { + "$ref": "../enums/rights-billing-period.json" + }, + "uses": { + "type": "array", + "items": { + "$ref": "../enums/right-use.json" + } + }, + "impression_cap": { + "type": "integer", + "minimum": 1 + }, + "overage_cpm": { + "type": "number", + "minimum": 0 + }, + "start_date": { + "type": "string", + "format": "date" + }, + "end_date": { + "type": "string", + "format": "date" + }, + "exclusivity": { + "type": "object", + "description": "Exclusivity terms if applicable", + "properties": { + "scope": { + "type": "string" + }, + "countries": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + } + }, + "additionalProperties": true + } + }, + "required": [ + "pricing_option_id", + "amount", + "currency", + "uses" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/search-brands-request.json b/schemas/cache/3.1.0-beta.5/brand/search-brands-request.json new file mode 100644 index 000000000..b884baf53 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/search-brands-request.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Search Brands Request", + "description": "Discover brands on a brand agent's roster using a natural language query. Returns lightweight brand stubs \u2014 use get_brand_identity to fetch full identity data for a selected brand_id, and get_rights to fetch pricing options. Read-only; naturally idempotent.", + "x-status": "experimental", + "type": "object", + "properties": { + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + }, + "query": { + "type": "string", + "description": "Natural language description of the brands to find. The agent interprets intent, category, and roster coverage from this text.", + "maxLength": 2000 + }, + "industries": { + "type": "array", + "description": "Optional industry filter. The agent restricts results to brands operating in these industries. Values follow the advertiser-industry taxonomy; the agent SHOULD accept unknown values gracefully per the advertiser-industry extension policy.", + "items": { + "$ref": "../enums/advertiser-industry.json" + }, + "minItems": 1 + }, + "countries": { + "type": "array", + "description": "Optional market filter. Returns brands with rights availability in at least one of the specified countries (ISO 3166-1 alpha-2).", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "buyer_brand": { + "$ref": "../core/brand-ref.json", + "description": "The buyer's brand. When provided, the agent SHOULD apply the same compatibility filtering as get_rights \u2014 filtering out brands with category conflicts, competitor exclusions, or dietary restrictions that would prevent licensing. When omitted, results are returned without compatibility filtering." + }, + "pagination": { + "$ref": "../core/pagination-request.json", + "description": "Pagination parameters for large result sets" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "query" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/search-brands-response.json b/schemas/cache/3.1.0-beta.5/brand/search-brands-response.json new file mode 100644 index 000000000..773401145 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/search-brands-response.json @@ -0,0 +1,223 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Search Brands Response", + "description": "Brand stubs matching the search query. Each stub carries the public identity tier \u2014 enough to evaluate a brand and follow up with get_brand_identity (for full identity) or get_rights (for pricing). Rich authorized data (voice synthesis, high-res assets, tone guidelines) is not included.", + "x-status": "experimental", + "definitions": { + "SearchBrandResult": { + "type": "object", + "description": "Lightweight brand stub returned by search_brands. Contains the public identity tier sufficient for discovery. Follow up with get_brand_identity for authorized fields.", + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier. Pass to get_brand_identity or get_rights for the next step.", + "x-entity": "advertiser_brand" + }, + "house": { + "type": "object", + "description": "The house (corporate entity) this brand belongs to.", + "properties": { + "domain": { + "type": "string", + "description": "House domain (e.g., acme-corp.com)" + }, + "name": { + "type": "string", + "description": "House display name" + } + }, + "required": [ + "domain", + "name" + ], + "additionalProperties": true + }, + "names": { + "type": "array", + "description": "Localized brand names with BCP 47 locale code keys (e.g., 'en_US', 'fr_CA').", + "items": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "string" + } + } + }, + "description": { + "type": "string", + "description": "Brand description" + }, + "industries": { + "type": "array", + "items": { + "$ref": "../enums/advertiser-industry.json" + }, + "minItems": 1, + "description": "Brand industries, following the advertiser-industry taxonomy" + }, + "keller_type": { + "type": "string", + "enum": [ + "master", + "sub_brand", + "endorsed", + "independent" + ], + "description": "Brand architecture type: master (primary brand of house), sub_brand (carries parent name), endorsed (independent identity backed by parent), independent (operates separately)" + }, + "logos": { + "type": "array", + "description": "Standard-resolution logos for display during discovery. High-res variants are gated behind get_brand_identity with account authorization.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL to the logo asset" + }, + "orientation": { + "type": "string", + "enum": [ + "square", + "horizontal", + "vertical", + "stacked" + ], + "description": "Logo aspect ratio orientation" + }, + "background": { + "type": "string", + "enum": [ + "dark-bg", + "light-bg", + "transparent-bg" + ], + "description": "Background compatibility" + }, + "variant": { + "type": "string", + "enum": [ + "primary", + "secondary", + "icon", + "wordmark", + "full-lockup" + ], + "description": "Logo variant type" + } + }, + "required": [ + "url" + ], + "additionalProperties": true + } + }, + "rights": { + "type": "object", + "description": "Rights availability summary. Signals whether this brand has licensable rights and in which markets. For detailed pricing and rights records, use get_rights.", + "properties": { + "available_uses": { + "type": "array", + "items": { + "$ref": "../enums/right-use.json" + } + }, + "countries": { + "type": "array", + "description": "Countries where rights are available (ISO 3166-1 alpha-2). If omitted, rights are available worldwide.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "excluded_countries": { + "type": "array", + "description": "Countries excluded from availability (ISO 3166-1 alpha-2)", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + } + }, + "additionalProperties": true + } + }, + "required": [ + "brand_id", + "house", + "names" + ], + "additionalProperties": true + } + }, + "type": "object", + "oneOf": [ + { + "title": "SearchBrandsSuccess", + "properties": { + "brands": { + "type": "array", + "description": "Brand stubs matching the query, ranked by relevance", + "items": { + "$ref": "#/definitions/SearchBrandResult" + } + }, + "pagination": { + "$ref": "../core/pagination-response.json", + "description": "Pagination metadata. Present when has_more is true or total_count is known." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "brands" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "SearchBrandsError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "required": [ + "brands" + ] + } + } + ], + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/update-rights-request.json b/schemas/cache/3.1.0-beta.5/brand/update-rights-request.json new file mode 100644 index 000000000..c25c95ff0 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/update-rights-request.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Update Rights Request", + "description": "Modify an existing rights grant \u2014 extend dates, adjust impression caps, change pricing, or pause/resume. Parallels update_media_buy. Only the fields provided are updated; omitted fields remain unchanged.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "x-status": "experimental", + "x-mutates-state": true, + "properties": { + "rights_id": { + "type": "string", + "description": "Rights grant identifier from acquire_rights response", + "x-entity": "rights_grant" + }, + "account": { + "$ref": "../core/account-ref.json", + "description": "Account context for this update. Used by the brand agent to resolve any governance agent previously bound for this brand+operator pair via sync_governance \u2014 update_rights is a modification-phase governance trigger (per `/docs/governance/campaign/specification#spend-commit-invocation`) and the brand agent consults the bound agent when computing the incremental commit delta. When both an inline governance_context token (on the protocol envelope) and a bound governance agent are present, the inline token wins. Pass a natural key (brand, operator, optional sandbox) or a seller-assigned account_id from list_accounts. The estimated_impressions / commit-delta projection rule for governance-aware updates is tracked separately and not yet normative on this task." + }, + "end_date": { + "type": "string", + "format": "date", + "description": "New end date for the rights grant (must be >= current end_date). Extending the grant may re-issue generation credentials with updated expiration." + }, + "impression_cap": { + "type": "integer", + "minimum": 1, + "description": "New impression cap for the grant. Must be >= impressions already delivered." + }, + "pricing_option_id": { + "type": "string", + "description": "Switch to a different pricing option from the original get_rights offering. The new option must be compatible with the existing grant's uses and countries.", + "x-entity": "vendor_pricing_option" + }, + "paused": { + "type": "boolean", + "description": "Pause or resume the rights grant. When paused, generation credentials are suspended and creative delivery should stop. When resumed, credentials are re-activated." + }, + "push_notification_config": { + "$ref": "../core/push-notification-config.json", + "description": "Webhook for async update notifications if the update requires approval" + }, + "idempotency_key": { + "type": "string", + "description": "Client-generated idempotency key for safe retries. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "idempotency_key", + "rights_id" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/update-rights-response.json b/schemas/cache/3.1.0-beta.5/brand/update-rights-response.json new file mode 100644 index 000000000..aef6371b3 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/update-rights-response.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Update Rights Response", + "description": "Result of a rights update request. Returns updated terms and re-issued credentials on success, or errors if the update cannot be applied.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "x-status": "experimental", + "oneOf": [ + { + "title": "UpdateRightsSuccess", + "properties": { + "rights_id": { + "type": "string", + "description": "Rights grant identifier", + "x-entity": "rights_grant" + }, + "terms": { + "$ref": "rights-terms.json", + "description": "Updated contractual terms (same shape as acquire_rights acquired response)" + }, + "generation_credentials": { + "type": "array", + "description": "Re-issued credentials reflecting updated terms (new expiration dates, adjusted caps)", + "items": { + "$ref": "../core/generation-credential.json" + } + }, + "rights_constraint": { + "$ref": "../core/rights-constraint.json", + "description": "Updated rights constraint for re-embedding in creative manifests" + }, + "paused": { + "type": "boolean", + "description": "Whether the grant is currently paused. Included when the update changes pause state." + }, + "implementation_date": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "When changes take effect (null if pending approval from rights holder)" + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "rights_id", + "terms" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "UpdateRightsError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "rights_id" + ] + }, + { + "required": [ + "terms" + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/verification-status.json b/schemas/cache/3.1.0-beta.5/brand/verification-status.json new file mode 100644 index 000000000..22918ba2c --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/verification-status.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Verification Status", + "description": "Status returned by `verify_brand_claim` for any claim_type (subsidiary, parent, property, trademark). Captures the rich state surface crawl-based mutual-assertion cannot express: pending review, in-flight transfers, disputed, licensed in/out, and explicit not-ours / unknown answers. Not every status applies to every claim_type \u2014 see the task page for the applicable subset per claim type.", + "type": "string", + "enum": [ + "owned", + "pending_review", + "transferring", + "disputed", + "not_ours", + "archived", + "licensed_in", + "licensed_out", + "unknown" + ], + "enumDescriptions": { + "owned": "Definitively belongs to this brand, currently.", + "archived": "The brand once held this but no longer does \u2014 divested subsidiary, expired/transferred trademark, sold-off property. Distinct from `not_ours` (never owned). Consumers MUST NOT extend present-tense governance trust through archived records, but the historical-record signal is useful for fraud detection, fair-use analysis, and recent-divestiture awareness. `details.context_note` MAY carry disposition (e.g., 'divested to X in 2024').", + "pending_review": "The brand is aware of this claim and has not yet decided. When returning this status, the agent MUST also return `expected_resolution_window_days` and MUST transition the claim to a terminal status (owned/disputed/not_ours) or flip to `unknown` once the window elapses. Consumers SHOULD NOT extend governance trust through pending claims.", + "transferring": "Ownership is provably changing \u2014 M&A in flight, divestiture closing, or a known imminent transition. Distinct from `pending_review` (under-review-by-us); `transferring` signals 'the answer is known to be becoming something else.' Consumers SHOULD treat as `owned` for stability until the agent moves to the new state, but MAY surface the in-flight state in UIs.", + "disputed": "The brand actively rejects this claim. Consumers MUST treat the claim as invalid and SHOULD surface the dispute (e.g., 'X says this is not theirs').", + "not_ours": "The brand affirms it is not their property / subsidiary / mark. Equivalent to 'disputed' but used when the brand-agent has no record of an existing claim \u2014 a clean 'we do not own this.'", + "licensed_in": "The brand uses this asset under license from another entity. The response carries `licensor_domain` when this status is returned. Applies to trademarks and (rarely) properties.", + "licensed_out": "The brand licenses this asset to another entity. Applies to trademarks; rare for properties or subsidiaries.", + "unknown": "The agent has no position. Caller MAY fall back to crawl-based mutual-assertion inference. Returned when the agent cannot classify the input (insufficient context, unrecognized identifier, out-of-scope query)." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/verify-brand-claim-request.json b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claim-request.json new file mode 100644 index 000000000..042059a1d --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claim-request.json @@ -0,0 +1,214 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Verify Brand Claim Request", + "description": "Ask a brand-agent a verification question \u2014 does the brand's own data confirm or reject a specific claim about a facet of its identity? The unified replacement for the per-dimension verify_* tools. claim_type discriminates the kind of question (subsidiary / parent / property / trademark); the claim payload carries the question. Read-only; naturally idempotent \u2014 the brand's internal bookkeeping deduplicates per {caller_identity, claim_type, claim-target}.", + "type": "object", + "discriminator": { + "propertyName": "claim_type" + }, + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "oneOf": [ + { + "title": "VerifySubsidiaryClaim", + "description": "House-side: is this brand a subsidiary of mine? Used by consumers detecting a leaf claiming `house_domain` pointing at this house.", + "properties": { + "claim_type": { + "type": "string", + "const": "subsidiary" + }, + "claim": { + "type": "object", + "properties": { + "subsidiary_domain": { + "type": "string", + "format": "hostname", + "description": "Domain of the leaf brand whose `house_domain` claim is being verified." + }, + "subsidiary_brand_id": { + "type": "string", + "pattern": "^[a-z0-9_]+$", + "description": "Stable brand identifier the leaf uses for itself. Optional but recommended." + }, + "observed_at": { + "type": "string", + "format": "date-time", + "description": "When the caller observed the leaf's claim." + } + }, + "required": [ + "subsidiary_domain" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + }, + { + "title": "VerifyParentClaim", + "description": "Leaf-side mirror: is this brand my parent house? The symmetric path that lets a leaf's brand-agent authoritatively confirm or reject a third party's claim about its parentage. Mutual assertion can complete at the agent layer (both house and leaf answer) rather than requiring a static-file crawl.", + "properties": { + "claim_type": { + "type": "string", + "const": "parent" + }, + "claim": { + "type": "object", + "properties": { + "parent_domain": { + "type": "string", + "format": "hostname", + "description": "Domain of the house claimed as this brand's parent." + }, + "claimant_says": { + "type": "string", + "description": "Optional context \u2014 what the claimant published or said. Helps the agent disambiguate competing claims." + }, + "observed_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "parent_domain" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + }, + { + "title": "VerifyPropertyClaim", + "description": "Is this property (website, app, podcast, etc.) one of mine? Used at inventory-onboarding, creative-clearance, and fraud-escalation gates.", + "properties": { + "claim_type": { + "type": "string", + "const": "property" + }, + "claim": { + "type": "object", + "properties": { + "property": { + "type": "object", + "description": "Shape matches brand.json properties[] entry.", + "properties": { + "type": { + "type": "string", + "enum": [ + "website", + "mobile_app", + "ctv_app", + "desktop_app", + "dooh", + "podcast", + "radio", + "streaming_audio" + ] + }, + "identifier": { + "type": "string" + }, + "store": { + "type": "string", + "enum": [ + "apple", + "google", + "amazon", + "roku", + "fire_tv", + "samsung", + "lg", + "vizio", + "other" + ] + }, + "region": { + "type": "string" + } + }, + "required": [ + "type", + "identifier" + ], + "additionalProperties": true + }, + "use_case": { + "type": "string", + "maxLength": 100, + "description": "Optional caller-declared use case. The agent MAY scope its answer." + } + }, + "required": [ + "property" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + }, + { + "title": "VerifyTrademarkClaim", + "description": "Is this trademark mine? Returns ownership plus licensing posture and authorized use cases \u2014 the differentiator vs registry crawls.", + "properties": { + "claim_type": { + "type": "string", + "const": "trademark" + }, + "claim": { + "type": "object", + "properties": { + "mark": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "registry": { + "type": "string", + "maxLength": 50 + }, + "number": { + "type": "string", + "maxLength": 100 + }, + "countries": { + "type": "array", + "items": { + "type": "string", + "minLength": 2, + "maxLength": 2 + } + }, + "use_case": { + "type": "string", + "maxLength": 100 + } + }, + "required": [ + "mark" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + } + ] +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/verify-brand-claim-response.json b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claim-response.json new file mode 100644 index 000000000..ab1d7d0f0 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claim-response.json @@ -0,0 +1,104 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Verify Brand Claim Response", + "description": "The brand-agent's authoritative answer to a verification claim. Always returns claim_type + verification_status; richer per-claim-type fields ride on the `details` object (shape varies by claim_type \u2014 see the task page).", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "VerifyBrandClaimSuccess", + "type": "object", + "properties": { + "claim_type": { + "type": "string", + "enum": [ + "subsidiary", + "parent", + "property", + "trademark" + ], + "description": "Echoes the request's claim_type for caller-side routing." + }, + "verification_status": { + "$ref": "verification-status.json", + "description": "Verification status. Not every status applies to every claim_type \u2014 see the task page for the applicable subset. Renamed from `status` in 3.1 to free the top-level `status` key for the envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization (#4878)." + }, + "details": { + "type": "object", + "description": "Per-claim-type response fields. Shape varies \u2014 see the task page for each claim_type's expected fields. Examples: subsidiary returns brand_id + first_observed_by_house_at + expected_resolution_window_days; parent returns house_domain; property returns relationship + regions + use_case_authorization; trademark returns matched_registration + licensor_domain + countries + nice_classes + use_case_authorization.", + "additionalProperties": true + }, + "context_note": { + "type": "string", + "maxLength": 500, + "description": "Public \u2014 free-text context the brand chooses to surface. For disputed/transferring, typically carries the rationale or transition details." + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "claim_type", + "verification_status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "errors" + ] + } + ] + } + }, + { + "title": "VerifyBrandClaimError", + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "verification_status" + ] + }, + { + "required": [ + "claim_type" + ] + } + ] + } + } + ], + "properties": {} +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/verify-brand-claims-request.json b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claims-request.json new file mode 100644 index 000000000..2048b5fcf --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claims-request.json @@ -0,0 +1,235 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Verify Brand Claims Request (Bulk)", + "description": "Bulk variant of `verify_brand_claim` \u2014 ask a brand-agent to verify many claims in a single round-trip. Use when a caller (e.g., a crawler refreshing a brand portfolio, a creative-clearance pipeline batch, an inventory-onboarding scan) needs to verify 10s-100s of claims against one brand-agent and the per-call MCP overhead dominates. Each entry in `claims[]` carries the same `{ claim_type, claim }` shape as the single-target tool; the agent returns one result per claim in the same order (zip-by-index). Sibling to `verify_brand_claim`; the single-target tool remains the right choice for one-off verifications. Read-only; naturally idempotent \u2014 same per-claim dedup as the single-target tool (the brand's internal bookkeeping deduplicates per {caller_identity, claim_type, claim-target}).", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + } + ], + "properties": { + "claims": { + "type": "array", + "description": "Ordered list of verification claims. The agent MUST return `results[]` in the same order (positional zip-by-index). Maximum batch size is 100 per call; agents MAY enforce a lower limit and SHOULD advertise it via `get_adcp_capabilities` (see the task page).", + "minItems": 1, + "maxItems": 100, + "items": { + "$ref": "#/definitions/claim_entry" + } + } + }, + "required": [ + "claims" + ], + "additionalProperties": true, + "definitions": { + "claim_entry": { + "type": "object", + "description": "One verification claim. Shape mirrors the single-target `verify_brand_claim` request body (minus the version envelope, which lives on the batch).", + "discriminator": { + "propertyName": "claim_type" + }, + "oneOf": [ + { + "title": "VerifySubsidiaryClaim", + "description": "House-side: is this brand a subsidiary of mine?", + "properties": { + "claim_type": { + "type": "string", + "const": "subsidiary" + }, + "claim": { + "type": "object", + "properties": { + "subsidiary_domain": { + "type": "string", + "format": "hostname", + "description": "Domain of the leaf brand whose `house_domain` claim is being verified." + }, + "subsidiary_brand_id": { + "type": "string", + "pattern": "^[a-z0-9_]+$", + "description": "Stable brand identifier the leaf uses for itself. Optional but recommended." + }, + "observed_at": { + "type": "string", + "format": "date-time", + "description": "When the caller observed the leaf's claim." + } + }, + "required": [ + "subsidiary_domain" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + }, + { + "title": "VerifyParentClaim", + "description": "Leaf-side mirror: is this brand my parent house?", + "properties": { + "claim_type": { + "type": "string", + "const": "parent" + }, + "claim": { + "type": "object", + "properties": { + "parent_domain": { + "type": "string", + "format": "hostname", + "description": "Domain of the house claimed as this brand's parent." + }, + "claimant_says": { + "type": "string", + "description": "Optional context \u2014 what the claimant published or said." + }, + "observed_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "parent_domain" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + }, + { + "title": "VerifyPropertyClaim", + "description": "Is this property (website, app, podcast, etc.) one of mine?", + "properties": { + "claim_type": { + "type": "string", + "const": "property" + }, + "claim": { + "type": "object", + "properties": { + "property": { + "type": "object", + "description": "Shape matches brand.json properties[] entry.", + "properties": { + "type": { + "type": "string", + "enum": [ + "website", + "mobile_app", + "ctv_app", + "desktop_app", + "dooh", + "podcast", + "radio", + "streaming_audio" + ] + }, + "identifier": { + "type": "string" + }, + "store": { + "type": "string", + "enum": [ + "apple", + "google", + "amazon", + "roku", + "fire_tv", + "samsung", + "lg", + "vizio", + "other" + ] + }, + "region": { + "type": "string" + } + }, + "required": [ + "type", + "identifier" + ], + "additionalProperties": true + }, + "use_case": { + "type": "string", + "maxLength": 100, + "description": "Optional caller-declared use case." + } + }, + "required": [ + "property" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + }, + { + "title": "VerifyTrademarkClaim", + "description": "Is this trademark mine?", + "properties": { + "claim_type": { + "type": "string", + "const": "trademark" + }, + "claim": { + "type": "object", + "properties": { + "mark": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "registry": { + "type": "string", + "maxLength": 50 + }, + "number": { + "type": "string", + "maxLength": 100 + }, + "countries": { + "type": "array", + "items": { + "type": "string", + "minLength": 2, + "maxLength": 2 + } + }, + "use_case": { + "type": "string", + "maxLength": 100 + } + }, + "required": [ + "mark" + ], + "additionalProperties": true + } + }, + "required": [ + "claim_type", + "claim" + ], + "additionalProperties": true + } + ] + } + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/brand/verify-brand-claims-response.json b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claims-response.json new file mode 100644 index 000000000..0646de58e --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/brand/verify-brand-claims-response.json @@ -0,0 +1,163 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Verify Brand Claims Response (Bulk)", + "description": "Bulk response for `verify_brand_claims`. `results[]` is positionally aligned with the request's `claims[]` (zip-by-index). Per-result success carries `claim_type` + `status` + per-claim-type `details`; per-result failure carries an `error` field on that single result without failing the batch. Top-level `errors[]` is reserved for batch-level failures (auth, rate-limit, malformed request) \u2014 in that case `results` is absent. Top-level `Cache-Control` represents the lowest-common max-age across the batch; individual results MAY carry their own staleness signals out-of-band.", + "type": "object", + "allOf": [ + { + "$ref": "../core/version-envelope.json" + }, + { + "$ref": "../core/protocol-envelope.json" + } + ], + "oneOf": [ + { + "title": "VerifyBrandClaimsSuccess", + "type": "object", + "properties": { + "results": { + "type": "array", + "description": "Per-claim results, positionally aligned with the request's `claims[]`. Each entry is either a per-claim success (claim_type + status + optional details/context_note) or a per-claim error (error field only).", + "minItems": 1, + "items": { + "$ref": "#/definitions/result_entry" + } + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "results" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "errors" + ] + } + ] + } + }, + { + "title": "VerifyBrandClaimsError", + "type": "object", + "description": "Batch-level failure \u2014 the entire request was rejected. Use per-result `error` on the success arm for per-claim failures that should not fail the batch.", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "../core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "../core/context.json" + }, + "ext": { + "$ref": "../core/ext.json" + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "results" + ] + } + ] + } + } + ], + "properties": {}, + "definitions": { + "result_entry": { + "type": "object", + "description": "One entry in `results[]`. Either a per-claim success (claim_type + status + optional details/context_note) or a per-claim error (error field only). Mirrors the single-target `verify_brand_claim` response success arm shape.", + "oneOf": [ + { + "title": "VerifyBrandClaimsResultSuccess", + "type": "object", + "properties": { + "claim_type": { + "type": "string", + "enum": [ + "subsidiary", + "parent", + "property", + "trademark" + ], + "description": "Echoes the request item's claim_type for caller-side routing." + }, + "status": { + "$ref": "verification-status.json", + "description": "Verification status for this claim. Not every status applies to every claim_type \u2014 see the single-target task page for the applicable subset." + }, + "details": { + "type": "object", + "description": "Per-claim-type response fields. Shape varies \u2014 see the single-target task page for each claim_type's expected fields.", + "additionalProperties": true + }, + "context_note": { + "type": "string", + "maxLength": 500, + "description": "Public \u2014 free-text context the brand chooses to surface." + } + }, + "required": [ + "claim_type", + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "error" + ] + } + ] + } + }, + { + "title": "VerifyBrandClaimsResultError", + "type": "object", + "description": "Per-claim failure carried inline so a batch can return partial results. Distinct from the batch-level errors arm.", + "properties": { + "error": { + "$ref": "../core/error.json" + } + }, + "required": [ + "error" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "status" + ] + }, + { + "required": [ + "claim_type" + ] + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/calibrate-content-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/calibrate-content-request.json new file mode 100644 index 000000000..017d49181 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/calibrate-content-request.json @@ -0,0 +1,2285 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Calibrate Content Request", + "description": "Request parameters for evaluating content during calibration. Multi-turn dialogue is handled at the protocol layer via contextId.", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "x-mutates-state": true, + "properties": { + "standards_id": { + "type": "string", + "description": "Standards configuration to calibrate against" + }, + "artifact": { + "title": "Artifact", + "description": "Artifact to evaluate", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "idempotency_key": { + "type": "string", + "description": "Client-generated unique key for at-most-once execution. If a request with the same key has already been processed, the server returns the original response without re-processing. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotency_key", + "standards_id", + "artifact" + ], + "$defs": { + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.439Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/calibrate-content-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/calibrate-content-response.json new file mode 100644 index 000000000..ce5109367 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/calibrate-content-response.json @@ -0,0 +1,672 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Calibrate Content Response", + "description": "Response payload with verdict and detailed explanations for collaborative calibration", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "type": "object", + "description": "Success response with detailed calibration feedback", + "properties": { + "verdict": { + "title": "Binary Verdict", + "description": "Strictly two-outcome evaluation result used for overall record-level verdicts in content standards tasks. For per-feature breakdowns that include warning and unevaluated states, see feature-check-status.", + "type": "string", + "enum": [ + "pass", + "fail" + ], + "enumDescriptions": { + "pass": "The evaluated record meets all applicable content standards", + "fail": "The evaluated record failed one or more content standard checks" + } + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Model confidence in the verdict (0-1)" + }, + "explanation": { + "type": "string", + "description": "Detailed natural language explanation of the decision" + }, + "features": { + "type": "array", + "description": "Per-feature breakdown with explanations. Mirrors validate_content_delivery feature shape so calibration loops can correlate against production verdicts by policy_id.", + "items": { + "type": "object", + "properties": { + "feature_id": { + "type": "string", + "description": "Which feature was evaluated. Data features come from the content-standards feature catalog (e.g., 'brand_safety', 'brand_suitability', 'competitor_adjacency'). Record-level structural checks use reserved namespaces: 'record:malformed_artifact'. Reserved prefixes: 'record:', 'delivery:'." + }, + "status": { + "title": "Feature Check Status", + "description": "Per-feature evaluation outcome in content standards checks. For the two-outcome overall record verdict, see binary-verdict.", + "type": "string", + "enum": [ + "passed", + "failed", + "warning", + "unevaluated" + ], + "enumDescriptions": { + "passed": "Feature met the applicable content standard", + "failed": "Feature did not meet the applicable content standard", + "warning": "Feature is within tolerance but approaching a threshold \u2014 informational, not blocking", + "unevaluated": "Feature was not assessed in this evaluation run (e.g., required data not present)" + } + }, + "policy_id": { + "type": "string", + "description": "Policy ID that triggered this result. Enables the calibration loop to iterate on specific policies by correlating sample outcomes to policy ids.", + "x-entity": "governance_registry_policy" + }, + "explanation": { + "type": "string", + "description": "Human-readable explanation of why this feature passed or failed" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Optional evaluator confidence in this result (0-1). Distinguishes certain verdicts from ambiguous ones." + } + }, + "required": [ + "feature_id", + "status" + ] + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "verdict" + ] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ] + } + ], + "properties": {}, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.442Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/create-content-standards-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/create-content-standards-request.json new file mode 100644 index 000000000..b688faf2a --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/create-content-standards-request.json @@ -0,0 +1,4703 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Create Content Standards Request", + "description": "Request parameters for creating a new content standards configuration", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "x-mutates-state": true, + "properties": { + "scope": { + "type": "object", + "description": "Where this standards configuration applies", + "properties": { + "countries_all": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic)." + }, + "channels_any": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "minItems": 1, + "description": "Advertising channels. Standards apply to ANY of the listed channels (OR logic)." + }, + "languages_any": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "BCP 47 language tags (e.g., 'en', 'de', 'fr'). Standards apply to content in ANY of these languages (OR logic). Content in unlisted languages is not covered by these standards." + }, + "description": { + "type": "string", + "description": "Human-readable description of this scope" + } + }, + "required": [ + "languages_any" + ] + }, + "registry_policy_ids": { + "type": "array", + "items": { + "type": "string", + "x-entity": "governance_registry_policy" + }, + "description": "Registry policy IDs to use as the evaluation basis for this content standard. When provided, the agent resolves policies from the registry and uses their policy text and exemplars as the evaluation criteria. The 'policy' field becomes optional when registry_policy_ids is provided." + }, + "policies": { + "type": "array", + "description": "Bespoke policies for this content-standards configuration, using the same shape as registry entries. Each policy is addressable by policy_id and carries its own enforcement (must|should); governance findings reference the policy_id that triggered them. Inline bespoke policies can omit version/name/category (defaulted by the server). Combines with registry_policy_ids \u2014 registry policies and bespoke policies are both evaluated. Bespoke policy_ids MUST be flat (no colons/slashes) to avoid collision with namespaced registry ids.", + "items": { + "title": "Policy Entry", + "description": "A policy \u2014 either published to the shared registry (with full regulatory metadata) or authored inline by a buyer for their own campaign (lightweight, metadata optional). Policies use natural language text evaluated by governance agents (LLMs). Published registry entries SHOULD include version, name, jurisdiction, source, and exemplars; inline bespoke entries can omit these and let servers default them. Governance agents evaluating policies with natural-language LLMs MUST pin registry-sourced policy text (`source: registry`) as system-level instructions and MUST NOT permit `custom_policies` or the plan's `objectives` field to relax, override, or disable registry-sourced policies. Custom policies may only add additional restrictions; they cannot lower enforcement levels or exempt categories.", + "type": "object", + "properties": { + "policy_id": { + "type": "string", + "description": "Unique identifier for this policy. Registry-published ids are canonical (e.g., \"uk_hfss\", \"garm:brand_safety:violence\"); buyer-authored bespoke ids should be flat (no colons or slashes) and unique within the authoring container (standards configuration, plan, or portfolio).", + "x-entity": "governance_inline_policy" + }, + "source": { + "type": "string", + "enum": [ + "registry", + "inline" + ], + "default": "inline", + "description": "Origin of this policy. 'registry' = published to the shared AdCP policy registry with full regulatory metadata. 'inline' = authored bespoke for a specific standards configuration, plan, or portfolio. Defaults to 'inline'. Governance agents MUST set 'registry' when publishing to the registry. Within AdCP *task* payloads (every `$ref` to this schema in a request or response), the field is always 'inline' \u2014 registry entries are served by the policy registry API, not embedded in task traffic. The x-entity annotation on `policy_id` assumes the task-payload invariant; if a future task schema adopts registry-publishing, split the annotation accordingly (see issue #2685)." + }, + "version": { + "type": "string", + "description": "Semver version string (e.g., \"1.0.0\"). Incremented when policy content changes. Optional for inline bespoke policies \u2014 defaults to \"1.0.0\". SHOULD be provided for registry-published policies." + }, + "name": { + "type": "string", + "description": "Human-readable name (e.g., \"UK HFSS Restrictions\"). Optional for inline bespoke policies \u2014 servers MAY default to policy_id." + }, + "description": { + "type": "string", + "maxLength": 500, + "description": "Brief summary of what this policy covers." + }, + "category": { + "title": "Policy Category", + "description": "The nature of the obligation: regulation (legal requirement) or standard (best practice). Optional for inline bespoke policies \u2014 defaults to \"standard\".", + "type": "string", + "enum": [ + "regulation", + "standard" + ], + "enumDescriptions": { + "regulation": "Legal requirement with jurisdiction scope. Violations have legal consequences. Enforcement is hard (must).", + "standard": "Industry best practice, voluntary but recommended. Protects brand value and campaign effectiveness. Enforcement is soft (should)." + } + }, + "enforcement": { + "title": "Policy Enforcement Level", + "description": "How governance agents treat violations. Regulations are typically \"must\"; standards are typically \"should\".", + "type": "string", + "enum": [ + "must", + "should", + "may" + ], + "enumDescriptions": { + "must": "Legal requirement. Governance agents reject actions that violate this policy.", + "should": "Best practice. Governance agents warn on violations but do not block.", + "may": "Recommendation. Governance agents log for informational purposes only." + } + }, + "requires_human_review": { + "type": "boolean", + "default": false, + "description": "When true, plans subject to this policy MUST set plan.human_review_required = true. Use for policies that mandate human oversight of decisions affecting data subjects \u2014 e.g., GDPR Article 22 (solely automated decisions with legal or similarly significant effects) and EU AI Act Annex III high-risk categories (credit, insurance pricing, recruitment, housing allocation). Governance agents MUST escalate any plan action whose resolved policies include requires_human_review: true. Unlike `enforcement`, this flag applies as soon as the policy is resolved \u2014 it is NOT gated by `effective_date`. Art 22 GDPR and similar foundational obligations may predate an AI-Act-specific effective date; the human-review requirement fires regardless." + }, + "jurisdictions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-1 alpha-2 country codes where this policy applies. Empty array means the policy is not jurisdiction-specific." + }, + "region_aliases": { + "type": "object", + "description": "Named groups of jurisdictions for convenience (e.g., {\"EU\": [\"AT\",\"BE\",\"BG\",...]}). Governance agents expand aliases when matching against a plan's target jurisdictions.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "policy_categories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Regulatory categories this policy belongs to (e.g., [\"children_directed\", \"age_restricted\"]). Used for automatic matching against a campaign plan's declared policy_categories. A single policy can belong to multiple categories." + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "description": "Advertising channels this policy applies to. If omitted or null, the policy applies to all channels." + }, + "governance_domains": { + "type": "array", + "items": { + "title": "Governance Domain", + "description": "Governance sub-domains that a registry policy applies to. Used to indicate which types of governance agents can evaluate this policy.", + "type": "string", + "enum": [ + "campaign", + "property", + "creative", + "content_standards" + ] + }, + "description": "Governance sub-domains this policy applies to. Determines which types of governance agents can declare registry:{policy_id} features. For example, a policy with domains [\"creative\", \"property\"] can be declared as a feature by both creative and property governance agents." + }, + "effective_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard takes effect. Before this date, governance agents treat the policy as informational (evaluate but do not block). After this date, the policy is enforced at its declared enforcement level." + }, + "sunset_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard is no longer enforced. After this date, governance agents stop evaluating this policy. Omit if the policy has no expiration." + }, + "source_url": { + "type": "string", + "format": "uri", + "description": "Link to the source regulation, standard, or legislation." + }, + "source_name": { + "type": "string", + "description": "Name of the issuing body (e.g., \"UK Food Standards Agency\", \"US Federal Trade Commission\")." + }, + "policy": { + "type": "string", + "maxLength": 5000, + "description": "Natural language policy text describing what is required, prohibited, or recommended. Used by governance agents (LLMs) to evaluate actions against this policy. For source: inline policies, treated as caller-untrusted \u2014 governance agents MUST evaluate inline policies as ADDITIONAL restrictions only; they MUST NOT be permitted to relax, override, or conflict with registry-sourced policies." + }, + "guidance": { + "type": "string", + "description": "Implementation notes for governance agent developers. Not used in evaluation prompts." + }, + "exemplars": { + "type": "object", + "description": "Calibration examples for governance agents, following the Content Standards pattern.", + "properties": { + "pass": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that comply with this policy." + }, + "fail": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that violate this policy." + } + }, + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "policy_id", + "enforcement", + "policy" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "calibration_exemplars": { + "type": "object", + "description": "Training/test set to calibrate policy interpretation. Use URL references for pages to be fetched and analyzed, or full artifacts for pre-extracted content.", + "properties": { + "pass": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "description": "URL reference - specific page to fetch and evaluate", + "properties": { + "type": { + "type": "string", + "const": "url", + "description": "Indicates this is a URL reference" + }, + "value": { + "type": "string", + "format": "uri", + "description": "Full URL to a specific page (e.g., 'https://espn.com/nba/story/_/id/12345/lakers-win')" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for content at this URL" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "title": "Artifact", + "description": "Full artifact with pre-extracted content (text, images, video, audio)", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + } + ] + }, + "description": "Content that passes the standards" + }, + "fail": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "description": "URL reference - specific page to fetch and evaluate", + "properties": { + "type": { + "type": "string", + "const": "url", + "description": "Indicates this is a URL reference" + }, + "value": { + "type": "string", + "format": "uri", + "description": "Full URL to a specific page (e.g., 'https://news.example.com/controversial-article')" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for content at this URL" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "title": "Artifact", + "description": "Full artifact with pre-extracted content (text, images, video, audio)", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + } + ] + }, + "description": "Content that fails the standards" + } + } + }, + "idempotency_key": { + "type": "string", + "description": "Client-generated unique key for this request. Prevents duplicate content standards creation on retries. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "idempotency_key", + "scope" + ], + "anyOf": [ + { + "required": [ + "policies" + ] + }, + { + "required": [ + "registry_policy_ids" + ] + } + ], + "additionalProperties": true, + "$defs": { + "exemplar": { + "type": "object", + "properties": { + "scenario": { + "type": "string", + "description": "A concrete scenario describing an advertising action or configuration." + }, + "explanation": { + "type": "string", + "description": "Why this scenario passes or fails the policy." + } + }, + "required": [ + "scenario", + "explanation" + ], + "additionalProperties": false + }, + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "MediaChannel": { + "title": "Media Channel", + "description": "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.", + "type": "string", + "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" + ], + "enumDescriptions": { + "display": "Digital display advertising (banners, native, rich media) across web and app", + "olv": "Online video advertising outside CTV (pre-roll, outstream, in-app video)", + "social": "Social media platforms (Facebook, Instagram, TikTok, LinkedIn, etc.)", + "search": "Search engine advertising and search networks", + "ctv": "Connected TV and streaming on television screens", + "linear_tv": "Traditional broadcast and cable television", + "radio": "Traditional AM/FM radio broadcast", + "streaming_audio": "Digital audio streaming services (Spotify, Pandora, etc.)", + "podcast": "Podcast advertising (host-read or dynamically inserted)", + "dooh": "Digital out-of-home screens in public spaces", + "ooh": "Classic out-of-home (physical billboards, transit, etc.)", + "print": "Newspapers, magazines, and other print publications", + "cinema": "Movie theater advertising", + "email": "Email advertising and sponsored newsletter content", + "gaming": "In-game advertising across platforms", + "retail_media": "Retail media networks and commerce marketplaces (Amazon, Walmart, Instacart)", + "influencer": "Creator and influencer marketing partnerships", + "affiliate": "Affiliate networks, comparison sites, and performance-based partnerships", + "product_placement": "Product placement, branded content, and sponsorship integrations", + "sponsored_intelligence": "Sponsored Intelligence \u2014 advertising within AI assistants, AI search, and generative AI experiences via the reversed data flow" + } + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.455Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/create-content-standards-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/create-content-standards-response.json new file mode 100644 index 000000000..f634a0483 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/create-content-standards-response.json @@ -0,0 +1,608 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Create Content Standards Response", + "description": "Response payload for creating a content standards configuration", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "type": "object", + "description": "Success response - returns the created standards identifier", + "properties": { + "standards_id": { + "type": "string", + "description": "Unique identifier for the created standards configuration" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "standards_id" + ] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "conflicting_standards_id": { + "type": "string", + "description": "If the error is a scope conflict, the ID of the existing standards that conflict" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ] + } + ], + "properties": {}, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.459Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-content-standards-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-content-standards-request.json new file mode 100644 index 000000000..a4bc7196f --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-content-standards-request.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Content Standards Request", + "description": "Request parameters for retrieving content safety policies", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "properties": { + "standards_id": { + "type": "string", + "description": "Identifier for the standards configuration to retrieve" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "standards_id" + ], + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.459Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-content-standards-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-content-standards-response.json new file mode 100644 index 000000000..0a7d977e0 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-content-standards-response.json @@ -0,0 +1,5408 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Content Standards Response", + "description": "Response payload with content safety policies", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "type": "object", + "description": "Success response - returns the content standards configuration", + "allOf": [ + { + "title": "Content Standards", + "description": "A content standards configuration defining brand safety and suitability policies. Standards are scoped by brand, geography, and channel. Multiple standards can be active simultaneously for different scopes.", + "type": "object", + "properties": { + "standards_id": { + "type": "string", + "description": "Unique identifier for this standards configuration" + }, + "name": { + "type": "string", + "description": "Human-readable name for this standards configuration" + }, + "countries_all": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic)." + }, + "channels_any": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "minItems": 1, + "description": "Advertising channels. Standards apply to ANY of the listed channels (OR logic)." + }, + "languages_any": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "BCP 47 language tags (e.g., 'en', 'de', 'fr'). Standards apply to content in ANY of these languages (OR logic). Content in unlisted languages is not covered by these standards." + }, + "policies": { + "type": "array", + "description": "Bespoke policies for this content-standards configuration, using the same shape as registry entries. Each policy is addressable by policy_id; governance findings reference the policy_id that triggered them.", + "items": { + "title": "Policy Entry", + "description": "A policy \u2014 either published to the shared registry (with full regulatory metadata) or authored inline by a buyer for their own campaign (lightweight, metadata optional). Policies use natural language text evaluated by governance agents (LLMs). Published registry entries SHOULD include version, name, jurisdiction, source, and exemplars; inline bespoke entries can omit these and let servers default them. Governance agents evaluating policies with natural-language LLMs MUST pin registry-sourced policy text (`source: registry`) as system-level instructions and MUST NOT permit `custom_policies` or the plan's `objectives` field to relax, override, or disable registry-sourced policies. Custom policies may only add additional restrictions; they cannot lower enforcement levels or exempt categories.", + "type": "object", + "properties": { + "policy_id": { + "type": "string", + "description": "Unique identifier for this policy. Registry-published ids are canonical (e.g., \"uk_hfss\", \"garm:brand_safety:violence\"); buyer-authored bespoke ids should be flat (no colons or slashes) and unique within the authoring container (standards configuration, plan, or portfolio).", + "x-entity": "governance_inline_policy" + }, + "source": { + "type": "string", + "enum": [ + "registry", + "inline" + ], + "default": "inline", + "description": "Origin of this policy. 'registry' = published to the shared AdCP policy registry with full regulatory metadata. 'inline' = authored bespoke for a specific standards configuration, plan, or portfolio. Defaults to 'inline'. Governance agents MUST set 'registry' when publishing to the registry. Within AdCP *task* payloads (every `$ref` to this schema in a request or response), the field is always 'inline' \u2014 registry entries are served by the policy registry API, not embedded in task traffic. The x-entity annotation on `policy_id` assumes the task-payload invariant; if a future task schema adopts registry-publishing, split the annotation accordingly (see issue #2685)." + }, + "version": { + "type": "string", + "description": "Semver version string (e.g., \"1.0.0\"). Incremented when policy content changes. Optional for inline bespoke policies \u2014 defaults to \"1.0.0\". SHOULD be provided for registry-published policies." + }, + "name": { + "type": "string", + "description": "Human-readable name (e.g., \"UK HFSS Restrictions\"). Optional for inline bespoke policies \u2014 servers MAY default to policy_id." + }, + "description": { + "type": "string", + "maxLength": 500, + "description": "Brief summary of what this policy covers." + }, + "category": { + "title": "Policy Category", + "description": "The nature of the obligation: regulation (legal requirement) or standard (best practice). Optional for inline bespoke policies \u2014 defaults to \"standard\".", + "type": "string", + "enum": [ + "regulation", + "standard" + ], + "enumDescriptions": { + "regulation": "Legal requirement with jurisdiction scope. Violations have legal consequences. Enforcement is hard (must).", + "standard": "Industry best practice, voluntary but recommended. Protects brand value and campaign effectiveness. Enforcement is soft (should)." + } + }, + "enforcement": { + "title": "Policy Enforcement Level", + "description": "How governance agents treat violations. Regulations are typically \"must\"; standards are typically \"should\".", + "type": "string", + "enum": [ + "must", + "should", + "may" + ], + "enumDescriptions": { + "must": "Legal requirement. Governance agents reject actions that violate this policy.", + "should": "Best practice. Governance agents warn on violations but do not block.", + "may": "Recommendation. Governance agents log for informational purposes only." + } + }, + "requires_human_review": { + "type": "boolean", + "default": false, + "description": "When true, plans subject to this policy MUST set plan.human_review_required = true. Use for policies that mandate human oversight of decisions affecting data subjects \u2014 e.g., GDPR Article 22 (solely automated decisions with legal or similarly significant effects) and EU AI Act Annex III high-risk categories (credit, insurance pricing, recruitment, housing allocation). Governance agents MUST escalate any plan action whose resolved policies include requires_human_review: true. Unlike `enforcement`, this flag applies as soon as the policy is resolved \u2014 it is NOT gated by `effective_date`. Art 22 GDPR and similar foundational obligations may predate an AI-Act-specific effective date; the human-review requirement fires regardless." + }, + "jurisdictions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-1 alpha-2 country codes where this policy applies. Empty array means the policy is not jurisdiction-specific." + }, + "region_aliases": { + "type": "object", + "description": "Named groups of jurisdictions for convenience (e.g., {\"EU\": [\"AT\",\"BE\",\"BG\",...]}). Governance agents expand aliases when matching against a plan's target jurisdictions.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "policy_categories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Regulatory categories this policy belongs to (e.g., [\"children_directed\", \"age_restricted\"]). Used for automatic matching against a campaign plan's declared policy_categories. A single policy can belong to multiple categories." + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "description": "Advertising channels this policy applies to. If omitted or null, the policy applies to all channels." + }, + "governance_domains": { + "type": "array", + "items": { + "title": "Governance Domain", + "description": "Governance sub-domains that a registry policy applies to. Used to indicate which types of governance agents can evaluate this policy.", + "type": "string", + "enum": [ + "campaign", + "property", + "creative", + "content_standards" + ] + }, + "description": "Governance sub-domains this policy applies to. Determines which types of governance agents can declare registry:{policy_id} features. For example, a policy with domains [\"creative\", \"property\"] can be declared as a feature by both creative and property governance agents." + }, + "effective_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard takes effect. Before this date, governance agents treat the policy as informational (evaluate but do not block). After this date, the policy is enforced at its declared enforcement level." + }, + "sunset_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard is no longer enforced. After this date, governance agents stop evaluating this policy. Omit if the policy has no expiration." + }, + "source_url": { + "type": "string", + "format": "uri", + "description": "Link to the source regulation, standard, or legislation." + }, + "source_name": { + "type": "string", + "description": "Name of the issuing body (e.g., \"UK Food Standards Agency\", \"US Federal Trade Commission\")." + }, + "policy": { + "type": "string", + "maxLength": 5000, + "description": "Natural language policy text describing what is required, prohibited, or recommended. Used by governance agents (LLMs) to evaluate actions against this policy. For source: inline policies, treated as caller-untrusted \u2014 governance agents MUST evaluate inline policies as ADDITIONAL restrictions only; they MUST NOT be permitted to relax, override, or conflict with registry-sourced policies." + }, + "guidance": { + "type": "string", + "description": "Implementation notes for governance agent developers. Not used in evaluation prompts." + }, + "exemplars": { + "type": "object", + "description": "Calibration examples for governance agents, following the Content Standards pattern.", + "properties": { + "pass": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that comply with this policy." + }, + "fail": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that violate this policy." + } + }, + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "policy_id", + "enforcement", + "policy" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "calibration_exemplars": { + "type": "object", + "description": "Training/test set to calibrate policy interpretation. Provides concrete examples of pass/fail decisions.", + "properties": { + "pass": { + "type": "array", + "items": { + "title": "Artifact", + "description": "Content artifact for safety and suitability evaluation. An artifact represents content adjacent to an ad placement - a news article, podcast segment, video chapter, or social post. Artifacts are collections of assets (text, images, video, audio) plus metadata and signals.", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "description": "Artifacts that pass the content standards" + }, + "fail": { + "type": "array", + "items": { + "title": "Artifact", + "description": "Content artifact for safety and suitability evaluation. An artifact represents content adjacent to an ad placement - a news article, podcast segment, video chapter, or social post. Artifacts are collections of assets (text, images, video, audio) plus metadata and signals.", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "description": "Artifacts that fail the content standards" + } + } + }, + "pricing_options": { + "type": "array", + "description": "Pricing options for this content standards service. The buyer passes the selected pricing_option_id in report_usage for billing verification.", + "items": { + "title": "Vendor Pricing Option", + "description": "A pricing option offered by a vendor agent (signals, creative, governance). Combines pricing_option_id with the pricing model fields. Pass pricing_option_id in report_usage for billing verification. All vendor discovery responses return pricing_options as an array \u2014 vendors may offer multiple options (volume tiers, context-specific rates, different models per product line).", + "allOf": [ + { + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Opaque identifier for this pricing option, unique within the vendor agent. Pass this in report_usage to identify which pricing option was applied.", + "x-entity": "vendor_pricing_option" + } + }, + "required": [ + "pricing_option_id" + ] + }, + { + "title": "Vendor Pricing", + "description": "Pricing model for a vendor service. Discriminated by model: 'cpm' (fixed CPM), 'percent_of_media' (percentage of spend with optional CPM cap), 'flat_fee' (fixed charge per reporting period), 'per_unit' (fixed price per unit of work), or 'custom' (escape hatch for models not covered by the enumerated forms \u2014 requires a description and structured metadata).", + "type": "object", + "discriminator": { + "propertyName": "model" + }, + "oneOf": [ + { + "title": "CpmPricing", + "description": "Fixed cost per thousand impressions", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "cpm" + }, + "cpm": { + "type": "number", + "description": "Cost per thousand impressions", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "cpm", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PercentOfMediaPricing", + "description": "Percentage of media spend charged for this signal. When max_cpm is set, the effective rate is capped at that CPM \u2014 useful for platforms like The Trade Desk that use percent-of-media pricing with a CPM ceiling.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "percent_of_media" + }, + "percent": { + "type": "number", + "description": "Percentage of media spend, e.g. 15 = 15%", + "minimum": 0, + "maximum": 100 + }, + "max_cpm": { + "type": "number", + "description": "Optional CPM cap. When set, the effective charge is min(percent \u00d7 media_spend_per_mille, max_cpm).", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for the resulting charge", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "percent", + "currency" + ], + "additionalProperties": true + }, + { + "title": "FlatFeePricing", + "description": "Fixed charge per billing period, regardless of impressions or spend. Used for licensed data bundles and audience subscriptions.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "flat_fee" + }, + "amount": { + "type": "number", + "description": "Fixed charge for the billing period", + "minimum": 0 + }, + "period": { + "type": "string", + "enum": [ + "monthly", + "quarterly", + "annual", + "campaign" + ], + "description": "Billing period for the flat fee." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "amount", + "period", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PerUnitPricing", + "description": "Fixed price per unit of work. Used for creative transformation (per format), AI generation (per image, per token), and rendering (per variant). The unit field describes what is counted; unit_price is the cost per one unit.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "per_unit" + }, + "unit": { + "type": "string", + "description": "What is counted \u2014 e.g. 'format', 'image', 'token', 'variant', 'render', 'evaluation'." + }, + "unit_price": { + "type": "number", + "description": "Cost per one unit", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "unit", + "unit_price", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CustomPricing", + "description": "Escape hatch for pricing constructs that do not fit cpm, percent_of_media, flat_fee, or per_unit. Use when a vendor prices via performance kickers, tiered volume, hybrid formulas, outcome-sharing, or any other model the standard forms cannot express. Requires a human-readable description and a structured metadata object that captures the parameters a buyer needs to reason about the charge. Buyers SHOULD route custom pricing through operator review before commitment \u2014 automatic selection is not recommended.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "custom" + }, + "description": { + "type": "string", + "description": "Human-readable description of the custom pricing model. Buyers display this to the operator when requesting approval.", + "minLength": 1 + }, + "metadata": { + "type": "object", + "description": "Structured parameters for the custom model. Keys follow lowercase_snake_case. Values may be primitives, arrays, or nested objects. Must be sufficient for a human to understand the pricing basis and for a downstream system to reconstruct the charge. Vendors SHOULD include a `summary_for_operator` string (one or two sentences, suitable for display in a buyer's operator-review UI) so reviewers across vendors see a consistent prompt. Required operator-review fields (approver role, dollar threshold for automatic approval, escalation contact) MAY be surfaced via additional keys the buyer's review surface recognizes.", + "additionalProperties": true, + "minProperties": 1, + "properties": { + "summary_for_operator": { + "type": "string", + "description": "One or two sentences describing the pricing construct in plain language, displayed to the buyer's operator when requesting approval. Should not repeat the top-level `description` verbatim \u2014 summarize the charge mechanic instead (e.g., 'Base $12 CPM plus $0.50 per qualifying post-view conversion, capped at $45 CPM').", + "minLength": 1 + } + } + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. Present when the pricing resolves to a monetary charge in a specific currency.", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "description", + "metadata" + ], + "additionalProperties": true + } + ] + } + ] + }, + "minItems": 1 + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "standards_id" + ] + } + ], + "properties": { + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + } + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ] + } + ], + "properties": {}, + "$defs": { + "exemplar": { + "type": "object", + "properties": { + "scenario": { + "type": "string", + "description": "A concrete scenario describing an advertising action or configuration." + }, + "explanation": { + "type": "string", + "description": "Why this scenario passes or fails the policy." + } + }, + "required": [ + "scenario", + "explanation" + ], + "additionalProperties": false + }, + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "MediaChannel": { + "title": "Media Channel", + "description": "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.", + "type": "string", + "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" + ], + "enumDescriptions": { + "display": "Digital display advertising (banners, native, rich media) across web and app", + "olv": "Online video advertising outside CTV (pre-roll, outstream, in-app video)", + "social": "Social media platforms (Facebook, Instagram, TikTok, LinkedIn, etc.)", + "search": "Search engine advertising and search networks", + "ctv": "Connected TV and streaming on television screens", + "linear_tv": "Traditional broadcast and cable television", + "radio": "Traditional AM/FM radio broadcast", + "streaming_audio": "Digital audio streaming services (Spotify, Pandora, etc.)", + "podcast": "Podcast advertising (host-read or dynamically inserted)", + "dooh": "Digital out-of-home screens in public spaces", + "ooh": "Classic out-of-home (physical billboards, transit, etc.)", + "print": "Newspapers, magazines, and other print publications", + "cinema": "Movie theater advertising", + "email": "Email advertising and sponsored newsletter content", + "gaming": "In-game advertising across platforms", + "retail_media": "Retail media networks and commerce marketplaces (Amazon, Walmart, Instacart)", + "influencer": "Creator and influencer marketing partnerships", + "affiliate": "Affiliate networks, comparison sites, and performance-based partnerships", + "product_placement": "Product placement, branded content, and sponsorship integrations", + "sponsored_intelligence": "Sponsored Intelligence \u2014 advertising within AI assistants, AI search, and generative AI experiences via the reversed data flow" + } + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.474Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-media-buy-artifacts-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-media-buy-artifacts-request.json new file mode 100644 index 000000000..f05b37ee5 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-media-buy-artifacts-request.json @@ -0,0 +1,749 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Media Buy Artifacts Request", + "description": "Request parameters for retrieving content artifacts from a media buy for validation", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "properties": { + "account": { + "title": "Account Reference", + "description": "Filter artifacts to a specific account. When omitted, returns artifacts across all accessible accounts.", + "type": "object", + "oneOf": [ + { + "properties": { + "account_id": { + "type": "string", + "description": "Seller-assigned account identifier (from sync_accounts or list_accounts)", + "x-entity": "account" + } + }, + "required": [ + "account_id" + ], + "additionalProperties": false + }, + { + "properties": { + "brand": { + "title": "Brand Reference", + "description": "Brand reference identifying the advertiser", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "operator": { + "type": "string", + "description": "Domain of the entity operating on the brand's behalf. When the brand operates directly, this is the brand's domain.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "x-entity": "operator" + }, + "sandbox": { + "type": "boolean", + "description": "When true, references the sandbox account for this brand/operator pair. Defaults to false (production account).", + "default": false + } + }, + "required": [ + "brand", + "operator" + ], + "additionalProperties": false + } + ], + "examples": [ + { + "account_id": "acc_acme_001" + }, + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com" + }, + { + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com" + }, + { + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "sandbox": true + } + ] + }, + "media_buy_id": { + "type": "string", + "description": "Media buy to get artifacts from" + }, + "package_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Filter to specific packages within the media buy" + }, + "failures_only": { + "type": "boolean", + "description": "When true, only return artifacts where the seller's local model returned local_verdict: 'fail'. Useful for auditing false positives. Not useful when the seller does not run a local evaluation model (all verdicts are 'unevaluated').", + "default": false + }, + "time_range": { + "type": "object", + "description": "Filter to specific time period", + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "Start of time range (inclusive)" + }, + "end": { + "type": "string", + "format": "date-time", + "description": "End of time range (exclusive)" + } + } + }, + "pagination": { + "type": "object", + "description": "Pagination parameters. Uses higher limits than standard pagination because artifact result sets can be very large.", + "properties": { + "max_results": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 1000, + "description": "Maximum number of artifacts to return per page" + }, + "cursor": { + "type": "string", + "description": "Opaque cursor from a previous response to fetch the next page" + } + }, + "additionalProperties": false + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "media_buy_id" + ], + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.478Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-media-buy-artifacts-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-media-buy-artifacts-response.json new file mode 100644 index 000000000..43717462c --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/get-media-buy-artifacts-response.json @@ -0,0 +1,2926 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Get Media Buy Artifacts Response", + "description": "Response containing content artifacts from a media buy for validation", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "type": "object", + "description": "Success response with artifacts", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Media buy these artifacts belong to" + }, + "artifacts": { + "type": "array", + "description": "Delivery records with full artifact content", + "items": { + "type": "object", + "properties": { + "record_id": { + "type": "string", + "description": "Unique identifier for this delivery record" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the delivery occurred" + }, + "package_id": { + "type": "string", + "description": "Which package this delivery belongs to" + }, + "artifact": { + "title": "Artifact", + "description": "Full artifact with content assets", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code where delivery occurred" + }, + "channel": { + "type": "string", + "description": "Channel type (e.g., display, video, audio, social)" + }, + "brand_context": { + "type": "object", + "description": "Brand information for policy evaluation. Schema TBD - placeholder for brand identifiers.", + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier" + }, + "sku_id": { + "type": "string", + "description": "Product/SKU identifier if applicable" + } + } + }, + "local_verdict": { + "type": "string", + "enum": [ + "pass", + "fail", + "unevaluated" + ], + "description": "Seller's local model verdict for this artifact" + } + }, + "required": [ + "record_id", + "artifact" + ] + } + }, + "collection_info": { + "type": "object", + "description": "Information about artifact collection for this media buy. Sampling is configured at buy creation time \u2014 this reports what was actually collected.", + "properties": { + "total_deliveries": { + "type": "integer", + "description": "Total deliveries in the requested time range" + }, + "total_collected": { + "type": "integer", + "description": "Total artifacts collected (per the buy's sampling configuration)" + }, + "returned_count": { + "type": "integer", + "description": "Number of artifacts in this response (may be less than total_collected due to pagination or filters)" + }, + "effective_rate": { + "type": "number", + "description": "Actual collection rate achieved (total_collected / total_deliveries)" + } + } + }, + "pagination": { + "title": "Pagination Response", + "description": "Standard cursor-based pagination metadata for list responses", + "type": "object", + "properties": { + "has_more": { + "type": "boolean", + "description": "Whether more results are available beyond this page" + }, + "cursor": { + "type": "string", + "description": "Opaque cursor to pass in the next request to fetch the next page. Only present when has_more is true." + }, + "total_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of items matching the query across all pages. Optional because not all backends can efficiently compute this." + } + }, + "required": [ + "has_more" + ], + "additionalProperties": false + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "media_buy_id", + "artifacts" + ] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ] + } + ], + "properties": {}, + "$defs": { + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.482Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/list-content-standards-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/list-content-standards-request.json new file mode 100644 index 000000000..07e2fcb8e --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/list-content-standards-request.json @@ -0,0 +1,140 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "List Content Standards Request", + "description": "Request parameters for listing content standards configurations", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "properties": { + "channels": { + "type": "array", + "items": { + "title": "Media Channel", + "description": "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.", + "type": "string", + "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" + ], + "enumDescriptions": { + "display": "Digital display advertising (banners, native, rich media) across web and app", + "olv": "Online video advertising outside CTV (pre-roll, outstream, in-app video)", + "social": "Social media platforms (Facebook, Instagram, TikTok, LinkedIn, etc.)", + "search": "Search engine advertising and search networks", + "ctv": "Connected TV and streaming on television screens", + "linear_tv": "Traditional broadcast and cable television", + "radio": "Traditional AM/FM radio broadcast", + "streaming_audio": "Digital audio streaming services (Spotify, Pandora, etc.)", + "podcast": "Podcast advertising (host-read or dynamically inserted)", + "dooh": "Digital out-of-home screens in public spaces", + "ooh": "Classic out-of-home (physical billboards, transit, etc.)", + "print": "Newspapers, magazines, and other print publications", + "cinema": "Movie theater advertising", + "email": "Email advertising and sponsored newsletter content", + "gaming": "In-game advertising across platforms", + "retail_media": "Retail media networks and commerce marketplaces (Amazon, Walmart, Instacart)", + "influencer": "Creator and influencer marketing partnerships", + "affiliate": "Affiliate networks, comparison sites, and performance-based partnerships", + "product_placement": "Product placement, branded content, and sponsorship integrations", + "sponsored_intelligence": "Sponsored Intelligence \u2014 advertising within AI assistants, AI search, and generative AI experiences via the reversed data flow" + } + }, + "minItems": 1, + "description": "Filter by channel" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Filter by BCP 47 language tags" + }, + "countries": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Filter by ISO 3166-1 alpha-2 country codes" + }, + "pagination": { + "title": "Pagination Request", + "description": "Standard cursor-based pagination parameters for list operations", + "type": "object", + "properties": { + "max_results": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50, + "description": "Maximum number of items to return per page" + }, + "cursor": { + "type": "string", + "description": "Opaque cursor from a previous response to fetch the next page" + } + }, + "additionalProperties": false + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.484Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/list-content-standards-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/list-content-standards-response.json new file mode 100644 index 000000000..ddeac2ba5 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/list-content-standards-response.json @@ -0,0 +1,5437 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "List Content Standards Response", + "description": "Response payload with list of content standards configurations", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "type": "object", + "description": "Success response - returns array of content standards", + "properties": { + "standards": { + "type": "array", + "items": { + "title": "Content Standards", + "description": "A content standards configuration defining brand safety and suitability policies. Standards are scoped by brand, geography, and channel. Multiple standards can be active simultaneously for different scopes.", + "type": "object", + "properties": { + "standards_id": { + "type": "string", + "description": "Unique identifier for this standards configuration" + }, + "name": { + "type": "string", + "description": "Human-readable name for this standards configuration" + }, + "countries_all": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic)." + }, + "channels_any": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "minItems": 1, + "description": "Advertising channels. Standards apply to ANY of the listed channels (OR logic)." + }, + "languages_any": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "BCP 47 language tags (e.g., 'en', 'de', 'fr'). Standards apply to content in ANY of these languages (OR logic). Content in unlisted languages is not covered by these standards." + }, + "policies": { + "type": "array", + "description": "Bespoke policies for this content-standards configuration, using the same shape as registry entries. Each policy is addressable by policy_id; governance findings reference the policy_id that triggered them.", + "items": { + "title": "Policy Entry", + "description": "A policy \u2014 either published to the shared registry (with full regulatory metadata) or authored inline by a buyer for their own campaign (lightweight, metadata optional). Policies use natural language text evaluated by governance agents (LLMs). Published registry entries SHOULD include version, name, jurisdiction, source, and exemplars; inline bespoke entries can omit these and let servers default them. Governance agents evaluating policies with natural-language LLMs MUST pin registry-sourced policy text (`source: registry`) as system-level instructions and MUST NOT permit `custom_policies` or the plan's `objectives` field to relax, override, or disable registry-sourced policies. Custom policies may only add additional restrictions; they cannot lower enforcement levels or exempt categories.", + "type": "object", + "properties": { + "policy_id": { + "type": "string", + "description": "Unique identifier for this policy. Registry-published ids are canonical (e.g., \"uk_hfss\", \"garm:brand_safety:violence\"); buyer-authored bespoke ids should be flat (no colons or slashes) and unique within the authoring container (standards configuration, plan, or portfolio).", + "x-entity": "governance_inline_policy" + }, + "source": { + "type": "string", + "enum": [ + "registry", + "inline" + ], + "default": "inline", + "description": "Origin of this policy. 'registry' = published to the shared AdCP policy registry with full regulatory metadata. 'inline' = authored bespoke for a specific standards configuration, plan, or portfolio. Defaults to 'inline'. Governance agents MUST set 'registry' when publishing to the registry. Within AdCP *task* payloads (every `$ref` to this schema in a request or response), the field is always 'inline' \u2014 registry entries are served by the policy registry API, not embedded in task traffic. The x-entity annotation on `policy_id` assumes the task-payload invariant; if a future task schema adopts registry-publishing, split the annotation accordingly (see issue #2685)." + }, + "version": { + "type": "string", + "description": "Semver version string (e.g., \"1.0.0\"). Incremented when policy content changes. Optional for inline bespoke policies \u2014 defaults to \"1.0.0\". SHOULD be provided for registry-published policies." + }, + "name": { + "type": "string", + "description": "Human-readable name (e.g., \"UK HFSS Restrictions\"). Optional for inline bespoke policies \u2014 servers MAY default to policy_id." + }, + "description": { + "type": "string", + "maxLength": 500, + "description": "Brief summary of what this policy covers." + }, + "category": { + "title": "Policy Category", + "description": "The nature of the obligation: regulation (legal requirement) or standard (best practice). Optional for inline bespoke policies \u2014 defaults to \"standard\".", + "type": "string", + "enum": [ + "regulation", + "standard" + ], + "enumDescriptions": { + "regulation": "Legal requirement with jurisdiction scope. Violations have legal consequences. Enforcement is hard (must).", + "standard": "Industry best practice, voluntary but recommended. Protects brand value and campaign effectiveness. Enforcement is soft (should)." + } + }, + "enforcement": { + "title": "Policy Enforcement Level", + "description": "How governance agents treat violations. Regulations are typically \"must\"; standards are typically \"should\".", + "type": "string", + "enum": [ + "must", + "should", + "may" + ], + "enumDescriptions": { + "must": "Legal requirement. Governance agents reject actions that violate this policy.", + "should": "Best practice. Governance agents warn on violations but do not block.", + "may": "Recommendation. Governance agents log for informational purposes only." + } + }, + "requires_human_review": { + "type": "boolean", + "default": false, + "description": "When true, plans subject to this policy MUST set plan.human_review_required = true. Use for policies that mandate human oversight of decisions affecting data subjects \u2014 e.g., GDPR Article 22 (solely automated decisions with legal or similarly significant effects) and EU AI Act Annex III high-risk categories (credit, insurance pricing, recruitment, housing allocation). Governance agents MUST escalate any plan action whose resolved policies include requires_human_review: true. Unlike `enforcement`, this flag applies as soon as the policy is resolved \u2014 it is NOT gated by `effective_date`. Art 22 GDPR and similar foundational obligations may predate an AI-Act-specific effective date; the human-review requirement fires regardless." + }, + "jurisdictions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-1 alpha-2 country codes where this policy applies. Empty array means the policy is not jurisdiction-specific." + }, + "region_aliases": { + "type": "object", + "description": "Named groups of jurisdictions for convenience (e.g., {\"EU\": [\"AT\",\"BE\",\"BG\",...]}). Governance agents expand aliases when matching against a plan's target jurisdictions.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "policy_categories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Regulatory categories this policy belongs to (e.g., [\"children_directed\", \"age_restricted\"]). Used for automatic matching against a campaign plan's declared policy_categories. A single policy can belong to multiple categories." + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "description": "Advertising channels this policy applies to. If omitted or null, the policy applies to all channels." + }, + "governance_domains": { + "type": "array", + "items": { + "title": "Governance Domain", + "description": "Governance sub-domains that a registry policy applies to. Used to indicate which types of governance agents can evaluate this policy.", + "type": "string", + "enum": [ + "campaign", + "property", + "creative", + "content_standards" + ] + }, + "description": "Governance sub-domains this policy applies to. Determines which types of governance agents can declare registry:{policy_id} features. For example, a policy with domains [\"creative\", \"property\"] can be declared as a feature by both creative and property governance agents." + }, + "effective_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard takes effect. Before this date, governance agents treat the policy as informational (evaluate but do not block). After this date, the policy is enforced at its declared enforcement level." + }, + "sunset_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard is no longer enforced. After this date, governance agents stop evaluating this policy. Omit if the policy has no expiration." + }, + "source_url": { + "type": "string", + "format": "uri", + "description": "Link to the source regulation, standard, or legislation." + }, + "source_name": { + "type": "string", + "description": "Name of the issuing body (e.g., \"UK Food Standards Agency\", \"US Federal Trade Commission\")." + }, + "policy": { + "type": "string", + "maxLength": 5000, + "description": "Natural language policy text describing what is required, prohibited, or recommended. Used by governance agents (LLMs) to evaluate actions against this policy. For source: inline policies, treated as caller-untrusted \u2014 governance agents MUST evaluate inline policies as ADDITIONAL restrictions only; they MUST NOT be permitted to relax, override, or conflict with registry-sourced policies." + }, + "guidance": { + "type": "string", + "description": "Implementation notes for governance agent developers. Not used in evaluation prompts." + }, + "exemplars": { + "type": "object", + "description": "Calibration examples for governance agents, following the Content Standards pattern.", + "properties": { + "pass": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that comply with this policy." + }, + "fail": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that violate this policy." + } + }, + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "policy_id", + "enforcement", + "policy" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "calibration_exemplars": { + "type": "object", + "description": "Training/test set to calibrate policy interpretation. Provides concrete examples of pass/fail decisions.", + "properties": { + "pass": { + "type": "array", + "items": { + "title": "Artifact", + "description": "Content artifact for safety and suitability evaluation. An artifact represents content adjacent to an ad placement - a news article, podcast segment, video chapter, or social post. Artifacts are collections of assets (text, images, video, audio) plus metadata and signals.", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "description": "Artifacts that pass the content standards" + }, + "fail": { + "type": "array", + "items": { + "title": "Artifact", + "description": "Content artifact for safety and suitability evaluation. An artifact represents content adjacent to an ad placement - a news article, podcast segment, video chapter, or social post. Artifacts are collections of assets (text, images, video, audio) plus metadata and signals.", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "description": "Artifacts that fail the content standards" + } + } + }, + "pricing_options": { + "type": "array", + "description": "Pricing options for this content standards service. The buyer passes the selected pricing_option_id in report_usage for billing verification.", + "items": { + "title": "Vendor Pricing Option", + "description": "A pricing option offered by a vendor agent (signals, creative, governance). Combines pricing_option_id with the pricing model fields. Pass pricing_option_id in report_usage for billing verification. All vendor discovery responses return pricing_options as an array \u2014 vendors may offer multiple options (volume tiers, context-specific rates, different models per product line).", + "allOf": [ + { + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Opaque identifier for this pricing option, unique within the vendor agent. Pass this in report_usage to identify which pricing option was applied.", + "x-entity": "vendor_pricing_option" + } + }, + "required": [ + "pricing_option_id" + ] + }, + { + "title": "Vendor Pricing", + "description": "Pricing model for a vendor service. Discriminated by model: 'cpm' (fixed CPM), 'percent_of_media' (percentage of spend with optional CPM cap), 'flat_fee' (fixed charge per reporting period), 'per_unit' (fixed price per unit of work), or 'custom' (escape hatch for models not covered by the enumerated forms \u2014 requires a description and structured metadata).", + "type": "object", + "discriminator": { + "propertyName": "model" + }, + "oneOf": [ + { + "title": "CpmPricing", + "description": "Fixed cost per thousand impressions", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "cpm" + }, + "cpm": { + "type": "number", + "description": "Cost per thousand impressions", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "cpm", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PercentOfMediaPricing", + "description": "Percentage of media spend charged for this signal. When max_cpm is set, the effective rate is capped at that CPM \u2014 useful for platforms like The Trade Desk that use percent-of-media pricing with a CPM ceiling.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "percent_of_media" + }, + "percent": { + "type": "number", + "description": "Percentage of media spend, e.g. 15 = 15%", + "minimum": 0, + "maximum": 100 + }, + "max_cpm": { + "type": "number", + "description": "Optional CPM cap. When set, the effective charge is min(percent \u00d7 media_spend_per_mille, max_cpm).", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for the resulting charge", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "percent", + "currency" + ], + "additionalProperties": true + }, + { + "title": "FlatFeePricing", + "description": "Fixed charge per billing period, regardless of impressions or spend. Used for licensed data bundles and audience subscriptions.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "flat_fee" + }, + "amount": { + "type": "number", + "description": "Fixed charge for the billing period", + "minimum": 0 + }, + "period": { + "type": "string", + "enum": [ + "monthly", + "quarterly", + "annual", + "campaign" + ], + "description": "Billing period for the flat fee." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "amount", + "period", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PerUnitPricing", + "description": "Fixed price per unit of work. Used for creative transformation (per format), AI generation (per image, per token), and rendering (per variant). The unit field describes what is counted; unit_price is the cost per one unit.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "per_unit" + }, + "unit": { + "type": "string", + "description": "What is counted \u2014 e.g. 'format', 'image', 'token', 'variant', 'render', 'evaluation'." + }, + "unit_price": { + "type": "number", + "description": "Cost per one unit", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "unit", + "unit_price", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CustomPricing", + "description": "Escape hatch for pricing constructs that do not fit cpm, percent_of_media, flat_fee, or per_unit. Use when a vendor prices via performance kickers, tiered volume, hybrid formulas, outcome-sharing, or any other model the standard forms cannot express. Requires a human-readable description and a structured metadata object that captures the parameters a buyer needs to reason about the charge. Buyers SHOULD route custom pricing through operator review before commitment \u2014 automatic selection is not recommended.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "custom" + }, + "description": { + "type": "string", + "description": "Human-readable description of the custom pricing model. Buyers display this to the operator when requesting approval.", + "minLength": 1 + }, + "metadata": { + "type": "object", + "description": "Structured parameters for the custom model. Keys follow lowercase_snake_case. Values may be primitives, arrays, or nested objects. Must be sufficient for a human to understand the pricing basis and for a downstream system to reconstruct the charge. Vendors SHOULD include a `summary_for_operator` string (one or two sentences, suitable for display in a buyer's operator-review UI) so reviewers across vendors see a consistent prompt. Required operator-review fields (approver role, dollar threshold for automatic approval, escalation contact) MAY be surfaced via additional keys the buyer's review surface recognizes.", + "additionalProperties": true, + "minProperties": 1, + "properties": { + "summary_for_operator": { + "type": "string", + "description": "One or two sentences describing the pricing construct in plain language, displayed to the buyer's operator when requesting approval. Should not repeat the top-level `description` verbatim \u2014 summarize the charge mechanic instead (e.g., 'Base $12 CPM plus $0.50 per qualifying post-view conversion, capped at $45 CPM').", + "minLength": 1 + } + } + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. Present when the pricing resolves to a monetary charge in a specific currency.", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "description", + "metadata" + ], + "additionalProperties": true + } + ] + } + ] + }, + "minItems": 1 + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "standards_id" + ] + }, + "description": "Array of content standards configurations matching the filter criteria" + }, + "pagination": { + "title": "Pagination Response", + "description": "Standard cursor-based pagination metadata for list responses", + "type": "object", + "properties": { + "has_more": { + "type": "boolean", + "description": "Whether more results are available beyond this page" + }, + "cursor": { + "type": "string", + "description": "Opaque cursor to pass in the next request to fetch the next page. Only present when has_more is true." + }, + "total_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of items matching the query across all pages. Optional because not all backends can efficiently compute this." + } + }, + "required": [ + "has_more" + ], + "additionalProperties": false + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "standards" + ] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ] + } + ], + "properties": {}, + "$defs": { + "exemplar": { + "type": "object", + "properties": { + "scenario": { + "type": "string", + "description": "A concrete scenario describing an advertising action or configuration." + }, + "explanation": { + "type": "string", + "description": "Why this scenario passes or fails the policy." + } + }, + "required": [ + "scenario", + "explanation" + ], + "additionalProperties": false + }, + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "MediaChannel": { + "title": "Media Channel", + "description": "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.", + "type": "string", + "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" + ], + "enumDescriptions": { + "display": "Digital display advertising (banners, native, rich media) across web and app", + "olv": "Online video advertising outside CTV (pre-roll, outstream, in-app video)", + "social": "Social media platforms (Facebook, Instagram, TikTok, LinkedIn, etc.)", + "search": "Search engine advertising and search networks", + "ctv": "Connected TV and streaming on television screens", + "linear_tv": "Traditional broadcast and cable television", + "radio": "Traditional AM/FM radio broadcast", + "streaming_audio": "Digital audio streaming services (Spotify, Pandora, etc.)", + "podcast": "Podcast advertising (host-read or dynamically inserted)", + "dooh": "Digital out-of-home screens in public spaces", + "ooh": "Classic out-of-home (physical billboards, transit, etc.)", + "print": "Newspapers, magazines, and other print publications", + "cinema": "Movie theater advertising", + "email": "Email advertising and sponsored newsletter content", + "gaming": "In-game advertising across platforms", + "retail_media": "Retail media networks and commerce marketplaces (Amazon, Walmart, Instacart)", + "influencer": "Creator and influencer marketing partnerships", + "affiliate": "Affiliate networks, comparison sites, and performance-based partnerships", + "product_placement": "Product placement, branded content, and sponsorship integrations", + "sponsored_intelligence": "Sponsored Intelligence \u2014 advertising within AI assistants, AI search, and generative AI experiences via the reversed data flow" + } + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.491Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/update-content-standards-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/update-content-standards-request.json new file mode 100644 index 000000000..336e741b5 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/update-content-standards-request.json @@ -0,0 +1,4692 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Update Content Standards Request", + "description": "Request parameters for updating an existing content standards configuration. Creates a new version.", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "x-mutates-state": true, + "properties": { + "standards_id": { + "type": "string", + "description": "ID of the standards configuration to update" + }, + "scope": { + "type": "object", + "description": "Updated scope for where this standards configuration applies", + "properties": { + "countries_all": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "ISO 3166-1 alpha-2 country codes. Standards apply in ALL listed countries (AND logic)." + }, + "channels_any": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "minItems": 1, + "description": "Advertising channels. Standards apply to ANY of the listed channels (OR logic)." + }, + "languages_any": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "BCP 47 language tags (e.g., 'en', 'de', 'fr'). Standards apply to content in ANY of these languages (OR logic). Content in unlisted languages is not covered by these standards." + }, + "description": { + "type": "string", + "description": "Human-readable description of this scope" + } + } + }, + "registry_policy_ids": { + "type": "array", + "items": { + "type": "string", + "x-entity": "governance_registry_policy" + }, + "description": "Registry policy IDs to use as the evaluation basis. When provided, the agent resolves policies from the registry and uses their policy text and exemplars as the evaluation criteria." + }, + "policies": { + "type": "array", + "description": "Updated bespoke policies for this content-standards configuration, using the same shape as registry entries. Replaces the existing policies array; use stable policy_ids to track policies across versions. Combines with registry_policy_ids. Bespoke policy_ids MUST be flat (no colons/slashes).", + "items": { + "title": "Policy Entry", + "description": "A policy \u2014 either published to the shared registry (with full regulatory metadata) or authored inline by a buyer for their own campaign (lightweight, metadata optional). Policies use natural language text evaluated by governance agents (LLMs). Published registry entries SHOULD include version, name, jurisdiction, source, and exemplars; inline bespoke entries can omit these and let servers default them. Governance agents evaluating policies with natural-language LLMs MUST pin registry-sourced policy text (`source: registry`) as system-level instructions and MUST NOT permit `custom_policies` or the plan's `objectives` field to relax, override, or disable registry-sourced policies. Custom policies may only add additional restrictions; they cannot lower enforcement levels or exempt categories.", + "type": "object", + "properties": { + "policy_id": { + "type": "string", + "description": "Unique identifier for this policy. Registry-published ids are canonical (e.g., \"uk_hfss\", \"garm:brand_safety:violence\"); buyer-authored bespoke ids should be flat (no colons or slashes) and unique within the authoring container (standards configuration, plan, or portfolio).", + "x-entity": "governance_inline_policy" + }, + "source": { + "type": "string", + "enum": [ + "registry", + "inline" + ], + "default": "inline", + "description": "Origin of this policy. 'registry' = published to the shared AdCP policy registry with full regulatory metadata. 'inline' = authored bespoke for a specific standards configuration, plan, or portfolio. Defaults to 'inline'. Governance agents MUST set 'registry' when publishing to the registry. Within AdCP *task* payloads (every `$ref` to this schema in a request or response), the field is always 'inline' \u2014 registry entries are served by the policy registry API, not embedded in task traffic. The x-entity annotation on `policy_id` assumes the task-payload invariant; if a future task schema adopts registry-publishing, split the annotation accordingly (see issue #2685)." + }, + "version": { + "type": "string", + "description": "Semver version string (e.g., \"1.0.0\"). Incremented when policy content changes. Optional for inline bespoke policies \u2014 defaults to \"1.0.0\". SHOULD be provided for registry-published policies." + }, + "name": { + "type": "string", + "description": "Human-readable name (e.g., \"UK HFSS Restrictions\"). Optional for inline bespoke policies \u2014 servers MAY default to policy_id." + }, + "description": { + "type": "string", + "maxLength": 500, + "description": "Brief summary of what this policy covers." + }, + "category": { + "title": "Policy Category", + "description": "The nature of the obligation: regulation (legal requirement) or standard (best practice). Optional for inline bespoke policies \u2014 defaults to \"standard\".", + "type": "string", + "enum": [ + "regulation", + "standard" + ], + "enumDescriptions": { + "regulation": "Legal requirement with jurisdiction scope. Violations have legal consequences. Enforcement is hard (must).", + "standard": "Industry best practice, voluntary but recommended. Protects brand value and campaign effectiveness. Enforcement is soft (should)." + } + }, + "enforcement": { + "title": "Policy Enforcement Level", + "description": "How governance agents treat violations. Regulations are typically \"must\"; standards are typically \"should\".", + "type": "string", + "enum": [ + "must", + "should", + "may" + ], + "enumDescriptions": { + "must": "Legal requirement. Governance agents reject actions that violate this policy.", + "should": "Best practice. Governance agents warn on violations but do not block.", + "may": "Recommendation. Governance agents log for informational purposes only." + } + }, + "requires_human_review": { + "type": "boolean", + "default": false, + "description": "When true, plans subject to this policy MUST set plan.human_review_required = true. Use for policies that mandate human oversight of decisions affecting data subjects \u2014 e.g., GDPR Article 22 (solely automated decisions with legal or similarly significant effects) and EU AI Act Annex III high-risk categories (credit, insurance pricing, recruitment, housing allocation). Governance agents MUST escalate any plan action whose resolved policies include requires_human_review: true. Unlike `enforcement`, this flag applies as soon as the policy is resolved \u2014 it is NOT gated by `effective_date`. Art 22 GDPR and similar foundational obligations may predate an AI-Act-specific effective date; the human-review requirement fires regardless." + }, + "jurisdictions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-1 alpha-2 country codes where this policy applies. Empty array means the policy is not jurisdiction-specific." + }, + "region_aliases": { + "type": "object", + "description": "Named groups of jurisdictions for convenience (e.g., {\"EU\": [\"AT\",\"BE\",\"BG\",...]}). Governance agents expand aliases when matching against a plan's target jurisdictions.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "policy_categories": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Regulatory categories this policy belongs to (e.g., [\"children_directed\", \"age_restricted\"]). Used for automatic matching against a campaign plan's declared policy_categories. A single policy can belong to multiple categories." + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "description": "Advertising channels this policy applies to. If omitted or null, the policy applies to all channels." + }, + "governance_domains": { + "type": "array", + "items": { + "title": "Governance Domain", + "description": "Governance sub-domains that a registry policy applies to. Used to indicate which types of governance agents can evaluate this policy.", + "type": "string", + "enum": [ + "campaign", + "property", + "creative", + "content_standards" + ] + }, + "description": "Governance sub-domains this policy applies to. Determines which types of governance agents can declare registry:{policy_id} features. For example, a policy with domains [\"creative\", \"property\"] can be declared as a feature by both creative and property governance agents." + }, + "effective_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard takes effect. Before this date, governance agents treat the policy as informational (evaluate but do not block). After this date, the policy is enforced at its declared enforcement level." + }, + "sunset_date": { + "type": "string", + "format": "date", + "description": "ISO 8601 date when the regulation or standard is no longer enforced. After this date, governance agents stop evaluating this policy. Omit if the policy has no expiration." + }, + "source_url": { + "type": "string", + "format": "uri", + "description": "Link to the source regulation, standard, or legislation." + }, + "source_name": { + "type": "string", + "description": "Name of the issuing body (e.g., \"UK Food Standards Agency\", \"US Federal Trade Commission\")." + }, + "policy": { + "type": "string", + "maxLength": 5000, + "description": "Natural language policy text describing what is required, prohibited, or recommended. Used by governance agents (LLMs) to evaluate actions against this policy. For source: inline policies, treated as caller-untrusted \u2014 governance agents MUST evaluate inline policies as ADDITIONAL restrictions only; they MUST NOT be permitted to relax, override, or conflict with registry-sourced policies." + }, + "guidance": { + "type": "string", + "description": "Implementation notes for governance agent developers. Not used in evaluation prompts." + }, + "exemplars": { + "type": "object", + "description": "Calibration examples for governance agents, following the Content Standards pattern.", + "properties": { + "pass": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that comply with this policy." + }, + "fail": { + "type": "array", + "items": { + "$ref": "#/$defs/exemplar" + }, + "description": "Scenarios that violate this policy." + } + }, + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "policy_id", + "enforcement", + "policy" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "calibration_exemplars": { + "type": "object", + "description": "Updated training/test set to calibrate policy interpretation. Use URL references for pages to be fetched and analyzed, or full artifacts for pre-extracted content.", + "properties": { + "pass": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "description": "URL reference - specific page to fetch and evaluate", + "properties": { + "type": { + "type": "string", + "const": "url", + "description": "Indicates this is a URL reference" + }, + "value": { + "type": "string", + "format": "uri", + "description": "Full URL to a specific page (e.g., 'https://espn.com/nba/story/_/id/12345/lakers-win')" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for content at this URL" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "title": "Artifact", + "description": "Full artifact with pre-extracted content (text, images, video, audio)", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + } + ] + }, + "description": "Content that passes the standards" + }, + "fail": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "description": "URL reference - specific page to fetch and evaluate", + "properties": { + "type": { + "type": "string", + "const": "url", + "description": "Indicates this is a URL reference" + }, + "value": { + "type": "string", + "format": "uri", + "description": "Full URL to a specific page (e.g., 'https://news.example.com/controversial-article')" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for content at this URL" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "title": "Artifact", + "description": "Full artifact with pre-extracted content (text, images, video, audio)", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + } + ] + }, + "description": "Content that fails the standards" + } + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + }, + "idempotency_key": { + "type": "string", + "description": "Client-generated unique key for at-most-once execution. If a request with the same key has already been processed, the server returns the original response without re-processing. MUST be unique per (seller, request) pair to prevent cross-seller correlation. Use a fresh UUID v4 for each request.", + "minLength": 16, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{16,255}$" + } + }, + "required": [ + "idempotency_key", + "standards_id" + ], + "additionalProperties": true, + "$defs": { + "exemplar": { + "type": "object", + "properties": { + "scenario": { + "type": "string", + "description": "A concrete scenario describing an advertising action or configuration." + }, + "explanation": { + "type": "string", + "description": "Why this scenario passes or fails the policy." + } + }, + "required": [ + "scenario", + "explanation" + ], + "additionalProperties": false + }, + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "MediaChannel": { + "title": "Media Channel", + "description": "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.", + "type": "string", + "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" + ], + "enumDescriptions": { + "display": "Digital display advertising (banners, native, rich media) across web and app", + "olv": "Online video advertising outside CTV (pre-roll, outstream, in-app video)", + "social": "Social media platforms (Facebook, Instagram, TikTok, LinkedIn, etc.)", + "search": "Search engine advertising and search networks", + "ctv": "Connected TV and streaming on television screens", + "linear_tv": "Traditional broadcast and cable television", + "radio": "Traditional AM/FM radio broadcast", + "streaming_audio": "Digital audio streaming services (Spotify, Pandora, etc.)", + "podcast": "Podcast advertising (host-read or dynamically inserted)", + "dooh": "Digital out-of-home screens in public spaces", + "ooh": "Classic out-of-home (physical billboards, transit, etc.)", + "print": "Newspapers, magazines, and other print publications", + "cinema": "Movie theater advertising", + "email": "Email advertising and sponsored newsletter content", + "gaming": "In-game advertising across platforms", + "retail_media": "Retail media networks and commerce marketplaces (Amazon, Walmart, Instacart)", + "influencer": "Creator and influencer marketing partnerships", + "affiliate": "Affiliate networks, comparison sites, and performance-based partnerships", + "product_placement": "Product placement, branded content, and sponsorship integrations", + "sponsored_intelligence": "Sponsored Intelligence \u2014 advertising within AI assistants, AI search, and generative AI experiences via the reversed data flow" + } + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.497Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/update-content-standards-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/update-content-standards-response.json new file mode 100644 index 000000000..6bec1be0e --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/update-content-standards-response.json @@ -0,0 +1,623 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Update Content Standards Response", + "description": "Response from updating a content standards configuration", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "title": "UpdateContentStandardsSuccess", + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true, + "description": "Indicates the update was applied successfully" + }, + "standards_id": { + "type": "string", + "description": "ID of the updated standards configuration" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "success", + "standards_id" + ], + "additionalProperties": true + }, + { + "title": "UpdateContentStandardsError", + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": false, + "description": "Indicates the update failed" + }, + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "description": "Errors that occurred during the update", + "minItems": 1 + }, + "conflicting_standards_id": { + "type": "string", + "description": "If scope change conflicts with another configuration, the ID of the conflicting standards" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "success", + "errors" + ], + "additionalProperties": true + } + ], + "properties": {}, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.499Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/validate-content-delivery-request.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/validate-content-delivery-request.json new file mode 100644 index 000000000..d87db6905 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/validate-content-delivery-request.json @@ -0,0 +1,2339 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Validate Content Delivery Request", + "description": "Request parameters for batch validating delivery records against content safety policies", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "properties": { + "standards_id": { + "type": "string", + "description": "Standards configuration to validate against" + }, + "records": { + "type": "array", + "description": "Delivery records to validate (max 10,000)", + "minItems": 1, + "maxItems": 10000, + "items": { + "type": "object", + "properties": { + "record_id": { + "type": "string", + "description": "Unique identifier for this delivery record" + }, + "media_buy_id": { + "type": "string", + "description": "Media buy this record belongs to (when batching across multiple buys)" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the delivery occurred" + }, + "artifact": { + "title": "Artifact", + "description": "Artifact where ad was delivered", + "type": "object", + "properties": { + "property_rid": { + "type": "string", + "description": "Stable property identifier from the property catalog. Globally unique across the ecosystem." + }, + "artifact_id": { + "type": "string", + "description": "Identifier for this artifact within the property. The property owner defines the scheme (e.g., 'article_12345', 'episode_42_segment_3', 'post_abc123')." + }, + "variant_id": { + "type": "string", + "description": "Identifies a specific variant of this artifact. Use for A/B tests, translations, or temporal versions. Examples: 'en', 'es-MX', 'v2', 'headline_test_b'. The combination of artifact_id + variant_id must be unique." + }, + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Always a structured object {agent_url, id} \u2014 never a plain string. Optional reference to a format definition. Uses the same format registry as creative formats.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional URL for this artifact (web page, podcast feed, video page). Not all artifacts have URLs (e.g., Instagram content, podcast segments, TV scenes)." + }, + "published_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was published (ISO 8601 format)" + }, + "last_update_time": { + "type": "string", + "format": "date-time", + "description": "When the artifact was last modified (ISO 8601 format)" + }, + "assets": { + "type": "array", + "description": "Artifact assets in document flow order - text blocks, images, video, audio", + "maxItems": 200, + "items": { + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "type": "object", + "description": "Text block (paragraph, heading, etc.)", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "role": { + "type": "string", + "enum": [ + "title", + "paragraph", + "heading", + "caption", + "quote", + "list_item", + "description" + ], + "description": "Role of this text in the document. Use 'title' for the main artifact title, 'description' for summaries." + }, + "content": { + "type": "string", + "description": "Text content. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 100000 + }, + "content_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "text/html", + "application/json" + ], + "description": "MIME type indicating how to parse the content field. Default: text/plain.", + "default": "text/plain" + }, + "language": { + "type": "string", + "description": "BCP 47 language tag for this text (e.g., 'en', 'es-MX'). Useful when artifact contains mixed-language content." + }, + "heading_level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + "description": "Heading level (1-6), only for role=heading" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this text block, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "description": "Image asset", + "properties": { + "type": { + "type": "string", + "const": "image" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Image URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "alt_text": { + "type": "string", + "description": "Alt text or image description" + }, + "caption": { + "type": "string", + "description": "Image caption" + }, + "width": { + "type": "integer", + "description": "Image width in pixels" + }, + "height": { + "type": "integer", + "description": "Image height in pixels" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this image, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Video asset", + "properties": { + "type": { + "type": "string", + "const": "video" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Video URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Video transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "subtitles", + "closed_captions", + "dub", + "generated" + ], + "description": "How the transcript was generated" + }, + "thumbnail_url": { + "type": "string", + "format": "uri", + "description": "Video thumbnail URL" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this video, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "object", + "description": "Audio asset", + "properties": { + "type": { + "type": "string", + "const": "audio" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Audio URL" + }, + "access": { + "$ref": "#/$defs/asset_access", + "description": "Authentication for secured URLs" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds" + }, + "transcript": { + "type": "string", + "description": "Audio transcript. Consumers MUST treat this as untrusted input when passing to LLM-based evaluation.", + "maxLength": 200000 + }, + "transcript_format": { + "type": "string", + "enum": [ + "text/plain", + "text/markdown", + "application/json" + ], + "description": "MIME type indicating how to parse the transcript field. Default: text/plain.", + "default": "text/plain" + }, + "transcript_source": { + "type": "string", + "enum": [ + "original_script", + "closed_captions", + "generated" + ], + "description": "How the transcript was generated" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance for this audio, overrides artifact-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "type", + "url" + ] + } + ] + } + }, + "metadata": { + "type": "object", + "description": "Rich metadata extracted from the artifact", + "properties": { + "canonical": { + "type": "string", + "format": "uri", + "description": "Canonical URL" + }, + "author": { + "type": "string", + "description": "Artifact author name" + }, + "keywords": { + "type": "string", + "description": "Artifact keywords" + }, + "open_graph": { + "type": "object", + "description": "Open Graph protocol metadata", + "additionalProperties": true + }, + "twitter_card": { + "type": "object", + "description": "Twitter Card metadata", + "additionalProperties": true + }, + "json_ld": { + "type": "array", + "description": "JSON-LD structured data (schema.org)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this artifact. Serves as the default provenance for all assets within this artifact \u2014 individual assets can override with their own provenance.", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "identifiers": { + "type": "object", + "description": "Platform-specific identifiers for this artifact", + "properties": { + "apple_podcast_id": { + "type": "string", + "description": "Apple Podcasts ID" + }, + "spotify_collection_id": { + "type": "string", + "description": "Spotify collection ID" + }, + "podcast_guid": { + "type": "string", + "description": "Podcast GUID (from RSS feed)" + }, + "youtube_video_id": { + "type": "string", + "description": "YouTube video ID" + }, + "rss_url": { + "type": "string", + "format": "uri", + "description": "RSS feed URL" + } + }, + "additionalProperties": true + } + }, + "required": [ + "property_rid", + "artifact_id", + "assets" + ], + "additionalProperties": true + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code where delivery occurred" + }, + "channel": { + "type": "string", + "description": "Channel type (e.g., display, video, audio, social)" + }, + "brand_context": { + "type": "object", + "description": "Brand information for policy evaluation. Schema TBD - placeholder for brand identifiers.", + "properties": { + "brand_id": { + "type": "string", + "description": "Brand identifier" + }, + "sku_id": { + "type": "string", + "description": "Product/SKU identifier if applicable" + } + } + } + }, + "required": [ + "record_id", + "artifact" + ] + } + }, + "feature_ids": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Specific features to evaluate (defaults to all)" + }, + "include_passed": { + "type": "boolean", + "default": true, + "description": "Include passed records in results" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "standards_id", + "records" + ], + "$defs": { + "asset_access": { + "type": "object", + "description": "Authentication for accessing secured asset URLs", + "discriminator": { + "propertyName": "method" + }, + "oneOf": [ + { + "type": "object", + "description": "Bearer token authentication", + "properties": { + "method": { + "type": "string", + "const": "bearer_token" + }, + "token": { + "type": "string", + "description": "OAuth2 bearer token for Authorization header" + } + }, + "required": [ + "method", + "token" + ] + }, + { + "type": "object", + "description": "Service account authentication (GCP, AWS)", + "properties": { + "method": { + "type": "string", + "const": "service_account" + }, + "provider": { + "type": "string", + "enum": [ + "gcp", + "aws" + ], + "description": "Cloud provider" + }, + "credentials": { + "type": "object", + "description": "Service account credentials", + "additionalProperties": true + } + }, + "required": [ + "method", + "provider" + ] + }, + { + "type": "object", + "description": "Pre-signed URL (credentials embedded in URL)", + "properties": { + "method": { + "type": "string", + "const": "signed_url" + } + }, + "required": [ + "method" + ] + } + ] + }, + "DigitalSourceType": { + "title": "Digital Source Type", + "description": "IPTC-aligned classification of AI involvement in producing this content", + "type": "string", + "enum": [ + "digital_capture", + "digital_creation", + "trained_algorithmic_media", + "composite_with_trained_algorithmic_media", + "algorithmic_media", + "composite_capture", + "composite_synthetic", + "human_edits", + "data_driven_media" + ], + "enumDescriptions": { + "digital_capture": "Captured by a digital device (camera, scanner, screen recording) with no AI involvement", + "digital_creation": "Created by a human using digital tools (Photoshop, Illustrator, After Effects) without AI generation", + "trained_algorithmic_media": "Generated entirely by a trained AI model (DALL-E, Midjourney, Stable Diffusion, Sora)", + "composite_with_trained_algorithmic_media": "Human-created content combined with AI-generated elements (e.g., photo with AI background)", + "algorithmic_media": "Produced by deterministic algorithms without machine learning (procedural generation, rule-based systems)", + "composite_capture": "Multiple digital captures composited together without AI", + "composite_synthetic": "Composite of multiple elements where at least one is AI-generated (e.g., stock photo composited with AI-generated background)", + "human_edits": "Content augmented, corrected, or enhanced by humans using non-generative tools", + "data_driven_media": "Assembled from structured data feeds (DCO templates, product catalogs, weather-triggered variants)" + } + }, + "EmbeddedProvenanceMethod": { + "title": "Embedded Provenance Method", + "description": "How provenance data is carried within the content", + "type": "string", + "enum": [ + "manifest_wrapper", + "provenance_markers" + ], + "enumDescriptions": { + "manifest_wrapper": "A provenance manifest embedded in the file container per format-specific rules (e.g., JUMBF box in JPEG, C2PATextManifestWrapper in plaintext per C2PA Section A.7). The manifest travels with the file but is tied to the file's byte structure.", + "provenance_markers": "Invisible markers embedded within the content stream that encode or reference a provenance record. Designed to survive reformatting, copy-paste, CMS ingestion, and ad-server transcoding that breaks file-level bindings." + } + }, + "WatermarkMediaType": { + "title": "Watermark Media Type", + "description": "Media category of the watermarked content", + "type": "string", + "enum": [ + "audio", + "image", + "video", + "text" + ], + "enumDescriptions": { + "audio": "Watermark applied to audio content (e.g., spread-spectrum, echo hiding)", + "image": "Watermark applied to image content (e.g., spatial domain, frequency domain)", + "video": "Watermark applied to video content (e.g., per-frame image watermarking, temporal watermarking)", + "text": "Watermark applied to text content (e.g., synonym substitution, structural modification)" + } + }, + "C2PAWatermarkAction": { + "title": "C2PA Watermark Action", + "description": "C2PA action classification for this watermark", + "type": "string", + "enum": [ + "c2pa.watermarked.bound", + "c2pa.watermarked.unbound" + ], + "enumDescriptions": { + "c2pa.watermarked.bound": "Watermark linked to a C2PA manifest for this asset. The watermark and manifest are mutually reinforcing: the manifest references the watermark, and the watermark can be used to locate the manifest.", + "c2pa.watermarked.unbound": "Watermark independent of any C2PA manifest. Applied before any provenance signing event (e.g., by the AI generator at creation time) or in pipelines where no manifest is present." + } + }, + "DisclosurePersistence": { + "title": "Disclosure Persistence", + "description": "How long the disclosure must persist during content playback or display", + "type": "string", + "enum": [ + "continuous", + "initial", + "flexible" + ], + "enumDescriptions": { + "continuous": "Disclosure must remain visible or audible throughout the entire content display duration. For video and audio, this means the full playback duration. For static formats (display, DOOH), this means the full display slot. For DOOH specifically, 'content duration' means the ad's display slot within the rotation, not the screen's full rotation cycle.", + "initial": "Disclosure must appear at the start of content for a minimum duration before it may be removed. Pair with min_duration_ms in render_guidance or creative brief to specify the required duration.", + "flexible": "Disclosure presence is sufficient; placement timing and duration are at the publisher's discretion" + } + }, + "DisclosurePosition": { + "title": "Disclosure Position", + "description": "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.", + "type": "string", + "enum": [ + "prominent", + "footer", + "audio", + "subtitle", + "overlay", + "end_card", + "pre_roll", + "companion" + ] + } + }, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.501Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/content-standards/validate-content-delivery-response.json b/schemas/cache/3.1.0-beta.5/bundled/content-standards/validate-content-delivery-response.json new file mode 100644 index 000000000..1c1e51c7f --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/content-standards/validate-content-delivery-response.json @@ -0,0 +1,700 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Validate Content Delivery Response", + "description": "Response payload with per-record verdicts and optional feature breakdown", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "title": "Task Status", + "description": "Current task execution state. Indicates whether the task is completed, in progress (working), submitted for async processing, failed, or requires user input. REQUIRED on every task response envelope. Synchronous tasks (including read-only metadata calls like `get_adcp_capabilities`) MUST emit `status: \"completed\"`; async tasks emit `submitted`, `working`, `input-required`, etc. per their lifecycle. Agents MUST NOT emit the legacy task_status or response_status fields alongside this field \u2014 the status field is the single authoritative task state.", + "type": "string", + "enum": [ + "submitted", + "working", + "input-required", + "completed", + "canceled", + "failed", + "rejected", + "auth-required", + "unknown" + ], + "enumDescriptions": { + "submitted": "Task accepted and queued for long-running execution (hours to days). Client should poll with tasks/get or provide webhook_url at protocol level.", + "working": "Agent is actively processing the task, expect completion within 120 seconds", + "input-required": "Task is paused and waiting for input from the user (e.g., clarification, approval)", + "completed": "Task has been successfully completed", + "canceled": "Task was canceled by the user", + "failed": "Task failed due to an error during execution", + "rejected": "Task was rejected by the agent and was not started", + "auth-required": "Task requires authentication to proceed", + "unknown": "Task is in an unknown or indeterminate state" + } + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "title": "Authentication Scheme", + "description": "Legacy authentication schemes for the webhook auth block. Bearer: token sent in Authorization header. HMAC-SHA256: legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit the authentication block and use the RFC 9421 webhook signing profile (applicable on schemas where authentication is optional). Removed in AdCP 4.0.", + "type": "string", + "enum": [ + "Bearer", + "HMAC-SHA256" + ] + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "type": "object", + "description": "Success response", + "properties": { + "summary": { + "type": "object", + "description": "Summary counts across all records", + "properties": { + "total_records": { + "type": "integer" + }, + "passed_records": { + "type": "integer" + }, + "failed_records": { + "type": "integer" + } + }, + "required": [ + "total_records", + "passed_records", + "failed_records" + ] + }, + "results": { + "type": "array", + "description": "Per-record evaluation results", + "items": { + "type": "object", + "properties": { + "record_id": { + "type": "string", + "description": "Which delivery record was evaluated" + }, + "verdict": { + "title": "Binary Verdict", + "description": "Strictly two-outcome evaluation result used for overall record-level verdicts in content standards tasks. For per-feature breakdowns that include warning and unevaluated states, see feature-check-status.", + "type": "string", + "enum": [ + "pass", + "fail" + ], + "enumDescriptions": { + "pass": "The evaluated record meets all applicable content standards", + "fail": "The evaluated record failed one or more content standard checks" + } + }, + "features": { + "type": "array", + "description": "Per-feature breakdown. When present, SHOULD include all failed and warning features. MAY include passed features. Oracle pattern: exposes verdict + rule pointer, never the seller's threshold or the caller's submitted value (the seller authored the content standards).", + "items": { + "type": "object", + "properties": { + "feature_id": { + "type": "string", + "description": "Which feature was evaluated. Data features come from the content-standards feature catalog (e.g., 'brand_safety', 'brand_suitability', 'image_dpi'). Record-level structural checks use reserved namespaces: 'record:malformed_artifact', 'delivery:authorization'. Reserved prefixes: 'record:', 'delivery:'." + }, + "status": { + "title": "Feature Check Status", + "description": "Per-feature evaluation outcome in content standards checks. For the two-outcome overall record verdict, see binary-verdict.", + "type": "string", + "enum": [ + "passed", + "failed", + "warning", + "unevaluated" + ], + "enumDescriptions": { + "passed": "Feature met the applicable content standard", + "failed": "Feature did not meet the applicable content standard", + "warning": "Feature is within tolerance but approaching a threshold \u2014 informational, not blocking", + "unevaluated": "Feature was not assessed in this evaluation run (e.g., required data not present)" + } + }, + "policy_id": { + "type": "string", + "description": "Registry policy ID that triggered this result. Present when the result originates from a specific registry policy (e.g., GARM category, CSBS standard). Enables programmatic routing by looking up the policy in the registry.", + "x-entity": "governance_registry_policy" + }, + "explanation": { + "type": "string", + "description": "Directional human-readable explanation (e.g., 'Below minimum resolution for display placement'). Avoid quantitative thresholds \u2014 the evaluator is the oracle." + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Optional evaluator confidence in this result (0-1). Distinguishes certain verdicts from ambiguous ones." + } + }, + "required": [ + "feature_id", + "status" + ] + } + } + }, + "required": [ + "record_id", + "verdict" + ] + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "summary", + "results" + ] + }, + { + "type": "object", + "description": "Error response", + "properties": { + "errors": { + "type": "array", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ] + } + ], + "properties": {}, + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.503Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/core/tasks-get-request.json b/schemas/cache/3.1.0-beta.5/bundled/core/tasks-get-request.json new file mode 100644 index 000000000..18082372f --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/core/tasks-get-request.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Tasks Get Request", + "description": "Request parameters for retrieving a specific task by ID with optional conversation history across all AdCP domains", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + } + ], + "properties": { + "task_id": { + "type": "string", + "description": "Unique identifier of the task to retrieve", + "x-entity": "task" + }, + "include_history": { + "type": "boolean", + "default": false, + "description": "Include full conversation history for this task (may increase response size)" + }, + "include_result": { + "type": "boolean", + "default": false, + "description": "Include the task's result payload when status is completed. Defaults to false for lightweight status-only polls. When true, sellers MUST include result on the response when status is completed." + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "task_id" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Check task status", + "data": { + "task_id": "task_456" + } + }, + { + "description": "Get task with full conversation history", + "data": { + "task_id": "task_123", + "include_history": true + } + }, + { + "description": "Poll for task completion including result payload", + "data": { + "task_id": "task_456", + "include_result": true + } + } + ], + "_bundled": { + "generatedAt": "2026-05-26T09:44:17.504Z", + "note": "This is a bundled schema with all $ref resolved inline. For the modular version with references, use the parent directory." + } +} \ No newline at end of file diff --git a/schemas/cache/3.1.0-beta.5/bundled/core/tasks-get-response.json b/schemas/cache/3.1.0-beta.5/bundled/core/tasks-get-response.json new file mode 100644 index 000000000..e1a2b46f0 --- /dev/null +++ b/schemas/cache/3.1.0-beta.5/bundled/core/tasks-get-response.json @@ -0,0 +1,104976 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Tasks Get Response", + "description": "Response containing detailed information about a specific task including status and optional conversation history across all AdCP protocols", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "$ref": "#/$defs/AuthenticationScheme" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "properties": { + "task_id": { + "type": "string", + "description": "Unique identifier for this task", + "x-entity": "task" + }, + "task_type": { + "title": "Task Type", + "description": "Type of AdCP operation", + "type": "string", + "enum": [ + "create_media_buy", + "update_media_buy", + "sync_creatives", + "activate_signal", + "get_signals", + "create_property_list", + "update_property_list", + "get_property_list", + "list_property_lists", + "delete_property_list", + "sync_accounts", + "get_account_financials", + "get_creative_delivery", + "sync_event_sources", + "sync_audiences", + "sync_catalogs", + "log_event", + "get_brand_identity", + "search_brands", + "get_rights", + "acquire_rights" + ], + "enumDescriptions": { + "create_media_buy": "Media-buy domain: Create a new advertising campaign with one or more packages", + "update_media_buy": "Media-buy domain: Update campaign settings, package configuration, or delivery parameters", + "sync_creatives": "Media-buy domain: Sync creative assets to publisher's library with upsert semantics", + "activate_signal": "Signals domain: Activate an audience signal on a specific platform or account", + "get_signals": "Signals domain: Discover available audience signals based on natural language description", + "create_property_list": "Property domain: Create a new property list with filters and brand reference", + "update_property_list": "Property domain: Update an existing property list", + "get_property_list": "Property domain: Retrieve a property list with resolved properties", + "list_property_lists": "Property domain: List all accessible property lists", + "delete_property_list": "Property domain: Delete a property list", + "sync_accounts": "Account domain: Sync advertiser accounts with a seller using upsert semantics", + "get_account_financials": "Account domain: Query financial status of an operator-billed account (spend, credit, invoices)", + "get_creative_delivery": "Creative domain: Retrieve variant-level creative delivery data", + "sync_event_sources": "Media-buy domain: Configure event sources on an account with upsert semantics", + "sync_audiences": "Media-buy domain: Manage first-party CRM audiences on an account with delta upsert semantics", + "sync_catalogs": "Media-buy domain: Sync catalog feeds (products, inventory, stores, promotions, offerings) to a platform with approval workflow", + "log_event": "Media-buy domain: Send conversion or marketing events for attribution", + "get_brand_identity": "Brand domain: Retrieve brand identity data (logos, colors, tone, assets, voice config) from a brand agent", + "search_brands": "Brand domain: Discover brands on a brand agent's roster via natural language query; returns lightweight identity stubs", + "get_rights": "Brand domain: Search for licensable rights across a brand agent's roster with pricing", + "acquire_rights": "Brand domain: Acquire rights from a brand agent with contractual clearance and generation credentials" + }, + "notes": [ + "Task types map to specific AdCP task operations", + "Each task type belongs to the 'media-buy', 'signals', 'property', 'account', 'creative', or 'brand' domain", + "This enum is used in task management APIs (tasks/list, tasks/get) and webhook payloads", + "New task types require a minor version bump per semantic versioning" + ] + }, + "protocol": { + "$ref": "#/$defs/AdCPProtocol" + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the task was initially created (ISO 8601)" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "When the task was last updated (ISO 8601)" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When the task completed (ISO 8601, only for completed/failed/canceled tasks)" + }, + "has_webhook": { + "type": "boolean", + "description": "Whether this task has webhook configuration" + }, + "progress": { + "type": "object", + "description": "Progress information for long-running tasks", + "properties": { + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Completion percentage (0-100)" + }, + "current_step": { + "type": "string", + "description": "Current step or phase of the operation" + }, + "total_steps": { + "type": "integer", + "minimum": 1, + "description": "Total number of steps in the operation" + }, + "step_number": { + "type": "integer", + "minimum": 1, + "description": "Current step number" + } + }, + "additionalProperties": true + }, + "error": { + "type": "object", + "description": "Error details for failed tasks", + "properties": { + "code": { + "type": "string", + "description": "Error code for programmatic handling" + }, + "message": { + "type": "string", + "description": "Detailed error message" + }, + "details": { + "type": "object", + "description": "Additional error context", + "properties": { + "protocol": { + "$ref": "#/$defs/AdCPProtocol" + }, + "operation": { + "type": "string", + "description": "Specific operation that failed" + }, + "specific_context": { + "type": "object", + "description": "Domain-specific error context", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "history": { + "type": "array", + "description": "Complete conversation history for this task (only included if include_history was true in request)", + "items": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When this exchange occurred (ISO 8601)" + }, + "type": { + "type": "string", + "enum": [ + "request", + "response" + ], + "description": "Whether this was a request from client or response from server" + }, + "data": { + "type": "object", + "description": "The full request or response payload", + "additionalProperties": true + } + }, + "required": [ + "timestamp", + "type", + "data" + ], + "additionalProperties": true + } + }, + "result": { + "title": "AdCP Async Response Data", + "description": "Task-specific completion payload. Present when status is 'completed' and include_result was true in the request; absent otherwise. For failed tasks, use the error field instead. Uses the same anyOf union as the push-notification webhook result field.", + "anyOf": [ + { + "title": "Get Products Response", + "description": "Response payload for get_products task", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "$ref": "#/$defs/AuthenticationScheme" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "properties": { + "products": { + "type": "array", + "description": "Array of matching products", + "items": { + "title": "Product", + "description": "Represents available advertising inventory", + "type": "object", + "properties": { + "product_id": { + "type": "string", + "description": "Unique identifier for the product", + "x-entity": "product" + }, + "name": { + "type": "string", + "description": "Human-readable product name" + }, + "description": { + "type": "string", + "description": "Detailed description of the product and its inventory" + }, + "publisher_properties": { + "type": "array", + "description": "SDK implementers MUST enforce singular-only at runtime: each entry uses the singular `publisher_domain` form; the compact `publisher_domains[]` form is rejected on products. Codegen toolchains (json-schema-to-typescript, quicktype, datamodel-code-generator, openapi-typescript-codegen) often flatten the `allOf + $ref + not.required` restriction below poorly and may drop the rejection constraint silently, emitting an unrestricted type \u2014 runtime enforcement is the safety net. Publisher properties covered by this product. Buyers fetch actual property definitions from each publisher's adagents.json and validate agent authorization. Selection patterns mirror the authorization patterns in adagents.json for consistency. The compact `publisher_domains[]` form is reserved for adagents.json `authorized_agents[].publisher_properties[]` so that buy-side traffic-and-pricing flatteners can always treat each entry as exactly one publisher.", + "items": { + "allOf": [ + { + "title": "Publisher Property Selector", + "description": "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.", + "discriminator": { + "propertyName": "selection_type" + }, + "oneOf": [ + { + "type": "object", + "description": "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).", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` \u2014 exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "publisher_domains": { + "type": "array", + "description": "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.", + "items": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "minItems": 1, + "uniqueItems": true + }, + "selection_type": { + "type": "string", + "const": "all", + "description": "Discriminator indicating all properties from each addressed publisher are included" + } + }, + "required": [ + "selection_type" + ], + "allOf": [ + { + "not": { + "required": [ + "publisher_domain", + "publisher_domains" + ] + } + }, + { + "anyOf": [ + { + "required": [ + "publisher_domain" + ] + }, + { + "required": [ + "publisher_domains" + ] + } + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Select specific properties by ID. Single-publisher only \u2014 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.", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com').", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "selection_type": { + "type": "string", + "const": "by_id", + "description": "Discriminator indicating selection by specific property IDs" + }, + "property_ids": { + "type": "array", + "description": "Specific property IDs from the publisher's adagents.json", + "items": { + "title": "Property ID", + "description": "Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "property", + "examples": [ + "cnn_ctv_app", + "homepage", + "mobile_ios", + "instagram" + ] + }, + "minItems": 1 + } + }, + "required": [ + "publisher_domain", + "selection_type", + "property_ids" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json \u2014 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).", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` \u2014 exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "publisher_domains": { + "type": "array", + "description": "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.", + "items": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "minItems": 1, + "uniqueItems": true + }, + "selection_type": { + "type": "string", + "const": "by_tag", + "description": "Discriminator indicating selection by property tags" + }, + "property_tags": { + "type": "array", + "description": "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.", + "items": { + "title": "Property Tag", + "description": "Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "examples": [ + "ctv", + "premium", + "news", + "sports", + "meta_network", + "social_media" + ] + }, + "minItems": 1 + } + }, + "required": [ + "selection_type", + "property_tags" + ], + "allOf": [ + { + "not": { + "required": [ + "publisher_domain", + "publisher_domains" + ] + } + }, + { + "anyOf": [ + { + "required": [ + "publisher_domain" + ] + }, + { + "required": [ + "publisher_domains" + ] + } + ] + } + ], + "additionalProperties": true + } + ] + }, + { + "not": { + "required": [ + "publisher_domains" + ] + } + } + ] + }, + "minItems": 1 + }, + "channels": { + "type": "array", + "description": "Advertising channels this product is sold as. Products inherit from their properties' supported_channels but may narrow the scope. For example, a product covering YouTube properties might be sold as ['ctv'] even though those properties support ['olv', 'social', 'ctv'].", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "uniqueItems": true + }, + "format_ids": { + "type": "array", + "description": "Legacy named-format path: array of supported creative format IDs (structured format_id objects with agent_url and id). Products MUST carry `format_ids`, `format_options`, or BOTH; at least one is required. Named formats predate 3.1 and remain supported through the deprecation calendar (2027-Q4 floor / 2029-Q1 ceiling).\n\n**Dual emission**: A product MAY carry both `format_ids` and `format_options` simultaneously during the migration window. This is the recommended seller pattern \u2014 author once, SDK projects to both wire shapes via the [canonical mapping registry](/schemas/registries/v1-canonical-mapping.json), every buyer reads what it knows. When both are present, the two MUST refer to the SAME underlying format declaration (the `format_options[i]` narrows the canonical that the named format in `format_ids[i]` resolves to via the registry / explicit `canonical` field). SDKs that derive both shapes from one source guarantee this invariant; SDKs that don't MUST treat divergence as a build error and refuse to emit. **Buyer rule**: when both are present, prefer `format_options`; treat `format_ids` as fallback for legacy-format buyers. **Non-projectable formats**: when a named format has no clean 3.1+ format-option projection (no registry entry, no explicit `canonical` declaration on the named format, no structural match), SDKs MUST NOT emit `format_options` for that product \u2014 only `format_ids` ships, and the product remains legacy-format-only until the seller adds an explicit `canonical` field or files a registry entry.", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + } + }, + "format_options": { + "type": "array", + "minItems": 1, + "description": "3.1+ format-option path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, and platform_extensions. The 90% case is a single-element array (one canonical narrowed for the product). Multi-element use cases: a product that accepts EITHER a third-party-hosted creative (for example, externally served `html5`) OR an internal `display_tag`; a video product that accepts a hosted `video_hosted` upload OR a `video_vast` tag. Buyers pick which option they're shipping at `sync_creatives` time by aligning their manifest to the matching declaration's `format_kind` and slots.\n\nProducts MUST carry `format_ids`, `format_options`, or BOTH; at least one is required. See `format_ids` description for the dual-emission contract (same underlying declaration when both are present; SDK derives one from the other; buyers prefer `format_options` when both are present).\n\nWhen `placements[]` also declare `format_ids` or `format_options`, product-level formats are the upper bound for the sellable product. Placement-level formats narrow the product-wide accepted set for that placement; they MUST NOT introduce a format the product does not accept. Buyers compute the effective accepted set for a placement as the intersection of product-level and placement-level declarations. For format options, match publisher-declared options by `{ publisher_domain, format_option_id }`, match product-local options by `format_option_id` when `publisher_domain` is omitted, and otherwise match declarations with the same `format_kind` whose placement parameters narrow the product declaration. If a placement has no format declaration, it inherits the product-level formats.", + "items": { + "title": "Product Format Declaration", + "description": "Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, 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 \u2014 lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). v1 named formats remain supported through the deprecation cycle; v2 product-bound declarations are opt-in.\n\n**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 \u2014 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**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.", + "type": "object", + "required": [ + "format_kind", + "params" + ], + "discriminator": { + "propertyName": "format_kind" + }, + "properties": { + "format_option_id": { + "type": "string", + "description": "Stable identifier for this format declaration within its namespace. REQUIRED when the parent product's `format_options` contains multiple declarations sharing the same `format_kind` (so buyers can disambiguate which option a manifest targets via `manifest.format_option_ref`). SHOULD be set on EVERY `format_options[]` entry \u2014 not just when structurally required to break a `format_kind` collision \u2014 so V2-mental-model buyers can use the V2 authoring path (`PackageRequest.format_option_refs[]`, `creative-manifest.format_option_ref`) against the product. Publisher-catalog-backed options pair this with `publisher_domain`; product-local options omit `publisher_domain` and are selected by `format_option_id` within the target product. A product that ships without selectable `format_option_id` values on its `format_options[]` entries is structurally 3.1-conformant but is not V2-authorable: buyers fall back to v1 `format_ids[]` and lose the stable naming the V2 path was designed to provide. Sellers MUST reject V2 authoring against such products with `UNSUPPORTED_FEATURE` and `error.details.reason` set to `format_option_refs_not_published` per `package-request.json`. Format-internal (not a URI). Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + }, + "publisher_domain": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "description": "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." + }, + "display_name": { + "type": "string", + "description": "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 \u2014 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." + }, + "applies_to_channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "uniqueItems": true, + "description": "Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel \u2014 `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`." + }, + "seller_preference": { + "type": "string", + "enum": [ + "preferred", + "accepted", + "discouraged" + ], + "description": "Optional soft routing hint *within* a product's accepted set of formats \u2014 NOT an enforcement axis. `preferred` \u2014 seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` \u2014 supported on equal footing with other format_options (default when omitted); `discouraged` \u2014 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 \u2014 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." + }, + "canonical_formats_only": { + "type": "boolean", + "default": false, + "description": "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 \u2014 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` \u2014 explicit v2-only is more useful than silent absence." + }, + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, THIS seller's specific product declaration may not work as declared \u2014 even if the underlying canonical is stable. Use for beta runtime paths, forward-looking catalog entries the runtime doesn't yet honor, or experimental products where the seller wants buyer-side caution. Buyers reading `experimental: true` on a product declaration SHOULD prefer the legacy named-format path when a fallback exists for the same product (via `format_ids` on the parent product or via this declaration's `v1_format_ref`) and SHOULD validate via `validate_input` or a sandbox before routing production budget.\n\nIndependent of the canonical's own `experimental` flag \u2014 a stable canonical (e.g., `image`, `video_hosted`) can carry an experimental product declaration when the seller is shipping a new runtime path that isn't fully wired yet. Conversely, an experimental canonical (`sponsored_placement`, `responsive_creative`, `agent_placement`) MAY carry non-experimental product declarations where the seller's adopter contract is well-tested. Buyer SDKs SHOULD filter products with `experimental: true` from default views and offer an opt-in flag to surface them.\n\nReplaces the earlier `runtime_status` enum (`stable | preview | declared_only`) \u2014 same semantic ('use with caution') without the cognitive overhead of two stability axes." + }, + "format_shape": { + "type": "string", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, \u2026). Non-canonical values valid (validators MAY soft-warn) \u2014 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." + }, + "v1_format_ref": { + "type": "array", + "minItems": 1, + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "description": "Authoritative v2 \u2192 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 \u2014 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` \u2014 see the 'Narrows \u2014 formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` \u2287 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 \u2014 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 \u2014 opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` \u2014 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 \u2192 registry glob \u2192 structural match \u2192 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 \u2192 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 \u2026) and the v1 wire fragments into per-publisher namespaces \u2014 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 \u2014 `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 \u22651 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/` \u2014 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." + }, + "format_schema": { + "title": "Platform Extension Reference", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://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 \u2014 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)** \u2014 `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 \u2014 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 \u2014 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 \u22645 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient \u2014 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** \u2014 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 \u00a76 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 \u22648 AND `$ref` count \u2264256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes \u2014 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 \u226410 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget \u2264250 ms (exceeded budget \u2192 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.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "format_kind": { + "const": "custom" + } + }, + "required": [ + "format_kind" + ] + }, + "then": { + "required": [ + "format_shape", + "format_schema" + ], + "anyOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + }, + "else": { + "not": { + "anyOf": [ + { + "required": [ + "format_shape" + ] + }, + { + "required": [ + "format_schema" + ] + } + ] + } + } + }, + { + "$comment": "canonical_formats_only:true and v1_format_ref are mutually exclusive \u2014 a declaration EITHER asserts no v1 projection OR links to a v1 named format, never both.", + "not": { + "allOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + } + }, + { + "$comment": "Canonical-format product declarations use format_option_id; capability_id belongs only to creative-agent build capabilities.", + "not": { + "required": [ + "capability_id" + ] + } + } + ], + "oneOf": [ + { + "title": "Image Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image" + }, + "params": { + "title": "Canonical Format: Image", + "description": "Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters \u2014 covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "image_main", + "asset_type": "image", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image canonical. Buyer ships an image asset (file or hosted URL) plus optional headline, body text, primary text (long-form caption), CTA (typically constrained to an enum via `cta_values`), and clickthrough URL. Products MAY override the default \u2014 make `headline` required, narrow `cta` to a value enum, or remove slots the surface doesn't consume." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required image width in pixels \u2014 use for fixed-size slots (e.g., a 300\u00d7250 IAB MREC). For multi-size flexible slots (publisher MREC slot that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250), use `sizes[]` instead; for responsive slots that adapt to viewport, use `min_width`/`max_width`/`min_height`/`max_height`. The three modes are mutually exclusive \u2014 set exactly one of `(width+height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required image height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. Buyer ships an asset matching one of the listed sizes; SDK validates `assets.image_main.{width,height}` against the list (any-match). Mirrors OpenRTB `banner.format[]` semantics \u2014 one declaration with N accepted sizes is cleaner than N format_options entries. Mutually exclusive with `(width, height)` and with `min/max_width` + `min/max_height` ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width in pixels for responsive slots that adapt within a range (e.g., 'any width from 300 to 970'). Use with `max_width` (and optionally `min_height`/`max_height`). Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width in pixels for responsive slots. Pair with `min_width`. See `min_width` for size-mode mutual exclusion." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height in pixels for responsive slots. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height in pixels for responsive slots. Pair with `min_height`." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside `width`/`height`, must agree. When used with `sizes[]` or responsive ranges, narrows accepted entries to those matching the aspect ratio." + }, + "max_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "svg" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the image and its trackers must be served over HTTPS." + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across all canonicals (`image`, `video_hosted`, `audio_hosted` \u2014 replaces the earlier per-canonical `image_source` / `video_source` / `audio_source` fields). `buyer_uploaded` (default): buyer ships a pre-rendered asset. `publisher_host_recorded`: publisher's host records the asset (audio-specific; podcast host-read pattern). `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE asset at sync_creatives or build_creative time (generative-DSP pattern). `seller_human_designed`: seller's design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class).\n\nNot every value is meaningful on every canonical \u2014 `publisher_host_recorded` is audio-specific; on `image` or `video_hosted` it has no defined behavior. Adopters MUST select a value appropriate to the canonical's asset type. The `slots` declaration is the binding contract for what the buyer ships; `asset_source` is informational and lets buyers understand the production model when picking products." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces 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`)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "HTML5 Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "html5" + }, + "params": { + "title": "Canonical Format: HTML5 Banner", + "description": "Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "html5_bundle", + "asset_type": "zip", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for html5 canonical. Buyer ships a zip bundle plus optional backup image (required when `backup_image_required: true`) and clickthrough URL. The zip's entry point is typically `index.html`; click handling uses the `clickTag` (or `clickTAG`) macro substituted by the seller at serve time." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required banner width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required banner height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot (publisher banner that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250). Mirrors OpenRTB `banner.format[]`. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive HTML5 banners that adapt within a range. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive HTML5 banners. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive HTML5 banners. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive HTML5 banners. Pair with `min_height`." + }, + "max_initial_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile." + }, + "max_polite_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes." + }, + "host_initiated_subload": { + "type": "boolean", + "description": "Whether the host page must initiate the polite-load phase. IAB-compliant banners require true." + }, + "max_animation_duration_ms": { + "type": "integer", + "minimum": 0, + "description": "Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds)." + }, + "max_cpu_load_percent": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Maximum CPU load percentage during render." + }, + "mraid_required": { + "type": "boolean", + "description": "Whether MRAID compatibility is required (mobile in-app)." + }, + "mraid_version": { + "type": "string", + "enum": [ + "2.0", + "3.0" + ], + "description": "Required MRAID version when mraid_required is true." + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether IAB Open Measurement SDK integration is required." + }, + "clicktag_macro": { + "type": "string", + "enum": [ + "clickTag", + "clickTAG" + ], + "description": "Name of the click-tag macro the bundle must use." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the zip for non-HTML5 environments." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum backup image file size in kilobytes." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Display Tag Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "display_tag" + }, + "params": { + "title": "Canonical Format: Display Tag", + "description": "Third-party-served display tag (JS, iframe, or 1\u00d71 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 \u2014 third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "tag_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + } + ], + "description": "Default slots for display_tag canonical. Buyer ships a URL pointing at the third-party-served creative (JS, iframe, or 1\u00d71 redirect) plus an optional backup image. Click and impression macros are substituted into the tag URL by the seller using `universal_macros`." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. The buyer's third-party tag must render at one of the listed sizes; the seller picks which size to request at impression time. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive third-party tags. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive third-party tags. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive third-party tags. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive third-party tags. Pair with `min_height`." + }, + "supported_tag_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "iframe", + "javascript", + "1x1_redirect" + ] + }, + "description": "Tag delivery mechanisms accepted." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the tag URL must be HTTPS." + }, + "max_redirect_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum redirect chain depth permitted." + }, + "max_response_time_ms": { + "type": "integer", + "minimum": 1, + "description": "Maximum tag-server response time in milliseconds." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the tag for environments that cannot render the third-party tag." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1 + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Image Carousel Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image_carousel" + }, + "params": { + "title": "Canonical Format: Image Carousel", + "description": "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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 multi-card carousels (Meta carousel, Pinterest pin collections, Snap collection ads) weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "cards", + "asset_type": "card", + "required": true, + "min": 2, + "max": 10 + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image_carousel. The `cards` slot's value in the manifest is an array of [card-asset](/schemas/core/assets/card-asset.json) objects; `min` / `max` constrain card count." + }, + "card_aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5')." + }, + "min_cards": { + "type": "integer", + "minimum": 2, + "description": "Minimum card count (typical: 2 or 3)." + }, + "max_cards": { + "type": "integer", + "description": "Maximum card count (typical: 6, 10, or 35 depending on platform)." + }, + "allowed_card_media_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "Asset types each card's `media` field may carry. Default: ['image']. Polymorphic carousels (Meta) allow ['image', 'video']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card's media payload, not the card-asset itself (which is always asset_type: \"card\")." + }, + "allowed_card_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "DEPRECATED \u2014 alias for `allowed_card_media_asset_types`. Kept for back-compat; prefer the new field name. Removed in 5.0." + }, + "card_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "card_video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum length of the carousel-level primary text." + }, + "card_headline_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card headline character limit. Governs the `headline` field on each card-asset in the `cards` slot." + }, + "card_description_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars)." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Video", + "description": "Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) \u2014 receivers fire impression and click pixels at delivery time.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "video_main", + "asset_type": "video", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "companion_banner", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_hosted canonical. Buyer ships a video asset (file or hosted URL); optional headline, primary text (long-form caption), CTA (typically constrained via `cta_values`), brand_name (typical for vertical short-form), companion_banner (typical for horizontal instream), and clickthrough URL. Products MAY override or extend the default \u2014 e.g., remove `companion_banner` for short-form vertical, narrow `cta` to a value enum, mark `landing_page_url` as required." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ], + "description": "Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed)." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio. Inferred from orientation if omitted." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: when both `duration_ms_exact` and `duration_ms_range` ship on the same product, `duration_ms_exact` takes precedence \u2014 buyers MUST validate against the exact value and ignore the range. The range is treated as advisory metadata in that case (e.g., for UI display showing the broader product family). SDKs SHOULD lint a warning when both fields ship; producers SHOULD pick one." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship (see `duration_ms_range` description)." + }, + "video_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "h264", + "h265", + "vp8", + "vp9", + "av1", + "prores" + ] + } + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aac", + "mp3", + "opus", + "pcm" + ] + } + }, + "containers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp4", + "webm", + "mov" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_file_size_mb": { + "type": "integer", + "minimum": 1 + }, + "frame_rates": { + "type": "array", + "items": { + "type": "number" + } + }, + "captions": { + "type": "string", + "enum": [ + "required", + "recommended", + "not_required" + ] + }, + "om_sdk_required": { + "type": "boolean" + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + } + }, + "companion_banner_widths": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + }, + "description": "Permitted companion banner widths (instream video)." + }, + "companion_banner_heights": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across canonicals. See `image.json#asset_source` for the full semantics. `publisher_host_recorded` is audio-specific and has no defined behavior on video \u2014 adopters MUST select a value appropriate to the canonical." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the video." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "VAST Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_vast" + }, + "params": { + "title": "Canonical Format: VAST Video", + "description": "VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "vast_tag", + "asset_type": "vast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_vast canonical. Buyer ships a VAST tag (URL or inline XML, VAST 2.x-4.x) plus an optional clickthrough URL (which falls back to the VAST `ClickThrough` element when omitted). Tracking events are inherent to VAST and don't require explicit slots." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ] + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$" + }, + "vast_version": { + "type": "string", + "enum": [ + "2.0", + "3.0", + "4.0", + "4.1", + "4.2" + ], + "description": "Required VAST version." + }, + "vpaid_enabled": { + "type": "boolean", + "description": "Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads." + }, + "vpaid_version": { + "type": "string", + "enum": [ + "1.0", + "2.0" + ] + }, + "simid_supported": { + "type": "boolean", + "description": "Whether IAB SIMID interactive video extensions are supported." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "linear_required": { + "type": "boolean", + "description": "Whether the VAST creative must be linear (non-skippable in-stream)." + }, + "skippable_after_ms": { + "type": "integer", + "minimum": 0, + "description": "When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll)." + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum VAST wrapper redirect depth permitted." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Audio", + "description": "Direct audio creative \u2014 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) \u2014 there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "audio_main", + "asset_type": "audio", + "required": true + }, + { + "asset_group_id": "companion_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for buyer-uploaded audio. Host-read products override with a `script` (asset_type: text) or `creative_brief` (asset_type: brief) slot in place of `audio_main`, plus `asset_source: 'publisher_host_recorded'` and `buyer_asset_acceptance: 'rejected'`. TTS-from-script products override similarly with `asset_source: 'seller_pre_rendered_from_brief'`." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship on the same product. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp3", + "aac", + "wav", + "opus", + "flac" + ] + } + }, + "audio_sample_rates": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "audio_channels": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mono", + "stereo" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "loudness_lufs": { + "type": "number", + "description": "Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values." + }, + "loudness_tolerance_db": { + "type": "number", + "minimum": 0, + "description": "Permitted deviation from loudness_lufs in dB." + }, + "true_peak_dbfs": { + "type": "number", + "description": "Maximum true-peak level in dBFS (typical: -2)." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered audio bytes come from. Single shared enum across canonicals (see `image.json#asset_source` for the full semantics). `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. This value is audio-specific." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `asset_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`)." + }, + "companion_image_required": { + "type": "boolean" + }, + "companion_image_aspect_ratio": { + "type": "string" + }, + "companion_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "DAAST Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_daast" + }, + "params": { + "title": "Canonical Format: DAAST Audio", + "description": "DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "daast_tag", + "asset_type": "daast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for audio_daast canonical. Buyer ships a DAAST tag (URL or inline XML, 1.0 or 1.1) plus an optional clickthrough URL. Tracking events are inherent to DAAST and don't require explicit slots." + }, + "daast_version": { + "type": "string", + "enum": [ + "1.0", + "1.1" + ] + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "linear_required": { + "type": "boolean" + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0 + }, + "ssl_required": { + "type": "boolean" + }, + "companion_image_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Sponsored Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "sponsored_placement" + }, + "params": { + "title": "Canonical Format: Sponsored Placement (retail-media catalog-driven)", + "description": "Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset \u2014 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** \u2014 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 \u2014 buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot \u2014 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) \u2014 use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Single-image or single-video creative \u2014 use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing \u2014 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).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical covers 4 meaningfully different retail-media adapter contracts (Amazon SP, Criteo SP / CitrusAd SP, Pinterest Collection, generative-per-SKU). Adopter contracts vary; buyers MUST validate per-adapter behavior before routing budget. Promotion to non-experimental gated on the #4592 adapter-contract docs work." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 retail-media catalog placements weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural, not a registry-coverage gap." + }, + "slots": { + "default": [ + { + "asset_group_id": "source_catalog", + "required": true, + "asset_type": "catalog" + }, + { + "asset_group_id": "hero_asset", + "required": false, + "asset_type": "image" + }, + { + "asset_group_id": "landing_page_url", + "required": false, + "asset_type": "url" + } + ] + }, + "supported_catalog_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "product", + "store", + "offering", + "hotel", + "flight", + "vehicle", + "real_estate", + "education", + "destination", + "app", + "job", + "inventory" + ] + }, + "description": "Catalog types this product accepts." + }, + "min_items": { + "type": "integer", + "minimum": 1, + "description": "Minimum catalog item count buyer must supply." + }, + "max_items": { + "type": "integer", + "description": "Maximum items considered for placement." + }, + "fanout_mode": { + "type": "string", + "enum": [ + "per_item", + "multi_item_in_creative", + "single_item" + ], + "description": "How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item." + }, + "required_catalog_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])." + }, + "supported_id_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "asin", + "sku", + "gtin", + "offering_id", + "store_id", + "hotel_id", + "flight_id", + "vehicle_id", + "listing_id", + "program_id", + "destination_id", + "app_id", + "job_id" + ] + }, + "description": "Catalog identifier types the placement renders against." + }, + "hero_asset_supported": { + "type": "boolean", + "description": "Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern)." + }, + "item_production_model": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "How each per-item creative is produced. Covers the same production-source axis as `asset_source` on `image` / `video_hosted` / `audio_hosted` but with a 4-value subset \u2014 drops `publisher_host_recorded` because it's audio-specific and doesn't apply to retail-media catalog placements. SDK codegen MAY share a base enum and narrow per-canonical, or emit two distinct enums; either way the wire values overlap exactly for the 4 retained values. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer's catalog already contains rendered assets per item; the seller composes the placement using those assets. (\"Uploaded\" reads slightly off for catalog-keyed items where the buyer didn't actively upload bytes \u2014 the catalog ingestion already supplied them \u2014 but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller's design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief \u00d7 N catalog items \u2192 N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Native In-Feed Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "native_in_feed" + }, + "params": { + "title": "Canonical Format: Native In-Feed", + "description": "IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types \u2014 `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 \u2014 visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 \u2014 app/product rating), `price` (Data Asset type 6 \u2014 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 \u2014 type 4, downloads \u2014 type 5, saleprice \u2014 type 7, phone_number \u2014 type 8, address \u2014 type 9, desc2 \u2014 type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel \u2014 feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative \u2014 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) \u2014 use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Multi-card carousel \u2014 use `image_carousel`.\n- Video-first native units where the asset is a hosted video file \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": false, + "description": "Stable at 3.1 GA. Shape mirrors IAB OpenRTB Native 1.2 \u2014 the renderer contract is well-established across in-feed native and content-recommendation adopters." + }, + "v1_translatable": { + "default": true, + "description": "Translates to v1 named native formats (e.g., `native_standard`, `native_content`) via the projection registry. Sellers with existing v1 named native formats SHOULD point `v1_format_ref[]` at them." + }, + "slots": { + "default": [ + { + "asset_group_id": "title", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "main_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "icon", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "advertiser_name", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "sponsored_label", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "display_url", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "rating", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "price", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "impression_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "viewability_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "click_tracker", + "asset_type": "pixel_tracker", + "required": false + } + ], + "description": "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)." + }, + "title_max_chars": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the body_text slot. IAB native typical: 90 (mainline) to 140 (extended)." + }, + "cta_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the cta slot. Typical: 15\u201325." + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD']). When set, narrows the cta slot to a closed enum." + }, + "main_image_sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "Accepted (width, height) pairs for the main_image slot. Common IAB native sizes: 1200\u00d7627 (1.91:1), 1080\u00d71080 (1:1), 1080\u00d71350 (4:5)." + }, + "icon_size": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false, + "description": "Required (width, height) for the icon slot when present (typical: 80\u00d780 or 100\u00d7100)." + }, + "max_image_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes for main_image and icon." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether trackers, landing pages, and image URLs must be served over HTTPS." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered native assets come from. `publisher_host_recorded` is omitted (audio-specific and not meaningful for native). Other values mirror the shared production-source axis used on `image` / `video_hosted`. `buyer_uploaded` (default): buyer ships pre-rendered title/image/body. `seller_pre_rendered_from_brief`: buyer ships a brief, seller renders the native bundle. `agent_synthesized`: AI synthesis pipeline produces title + image + body from a brief; pair with `synthesis_nondeterministic: true` for generative pipelines that can't guarantee in-spec output." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded native assets. When `rejected`, the buyer cannot ship pre-rendered title/image/body \u2014 they must use `build_creative` (or `sync_creatives` with brief inputs) so the seller produces the native bundle from a brief." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Responsive Creative Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "responsive_creative" + }, + "params": { + "title": "Canonical Format: Responsive Creative", + "description": "Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** \u2014 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.).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: composition is algorithmic (the surface picks combinations and reports per-asset breakdowns), and there's no clean v1-translatable equivalent. Buyers ship asset pools rather than rendered creatives; the surface's per-impression composition cannot be predicted by `validate_input`. Adopters SHOULD validate behavior per surface (Google PMax vs Meta Advantage+ creative differ meaningfully)." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 algorithmic asset-pool composition (Google PMax / Meta Advantage+ creative) wasn't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "headlines", + "asset_type": "text", + "required": true, + "min": 3, + "max": 15 + }, + { + "asset_group_id": "long_headlines", + "asset_type": "text", + "required": false, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "descriptions", + "asset_type": "text", + "required": true, + "min": 2, + "max": 5 + }, + { + "asset_group_id": "images_landscape", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_square", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_vertical", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "video", + "asset_type": "video", + "required": false, + "min": 0, + "max": 5 + }, + { + "asset_group_id": "logo", + "asset_type": "image", + "required": true, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true, + "min": 1, + "max": 1 + } + ] + }, + "headlines_min": { + "type": "integer", + "minimum": 0 + }, + "headlines_max": { + "type": "integer", + "minimum": 0 + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "long_headlines_min": { + "type": "integer", + "minimum": 0 + }, + "long_headlines_max": { + "type": "integer", + "minimum": 0 + }, + "long_headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "descriptions_min": { + "type": "integer", + "minimum": 0 + }, + "descriptions_max": { + "type": "integer", + "minimum": 0 + }, + "description_max_chars": { + "type": "integer", + "minimum": 1 + }, + "images_landscape_min": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_max": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_aspect_ratio": { + "type": "string" + }, + "images_square_min": { + "type": "integer", + "minimum": 0 + }, + "images_square_max": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_min": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_max": { + "type": "integer", + "minimum": 0 + }, + "videos_min": { + "type": "integer", + "minimum": 0 + }, + "videos_max": { + "type": "integer", + "minimum": 0 + }, + "video_min_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "logo_min": { + "type": "integer", + "minimum": 0 + }, + "logo_max": { + "type": "integer", + "minimum": 0 + }, + "logo_aspect_ratios": { + "type": "array", + "items": { + "type": "string" + } + }, + "business_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "asset_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "supports_catalog_input": { + "type": "boolean", + "description": "Whether the product can additionally consume a catalog reference (e.g., PMax with product feed)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Agent Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "agent_placement" + }, + "params": { + "title": "Canonical Format: Agent Placement (AI-surface sponsored placement)", + "description": "**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 set `runtime_status: 'preview'` or `'declared_only'` on the declaration; 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** \u2014 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 \u2014 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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical's tracking model (mention-level impression + attribution, postback shape, cross-surface dedup) is intentionally underspecified for 3.1. Adopters claiming `agent_placement` ship private tracking integrations; buyer agents MUST treat attribution as adapter-defined until the 3.2 tracking-macro spec lands. Promotion to non-experimental gated on the 3.2 tracking-contract spec." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 AI-surface sponsored mentions weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "offering_ref", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "agent_placement has minimal buyer-shipped slots \u2014 the surface composes the rendered output from brand context (resolved via the manifest's top-level `brand` BrandRef) plus optional offering_ref and landing_page_url assets. None of these assets are rendered verbatim by the buyer; the agent chooses how to use them." + }, + "output_modality": { + "type": "string", + "enum": [ + "text", + "audio", + "card" + ], + "description": "How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text." + }, + "max_mention_length_chars": { + "type": "integer", + "minimum": 1, + "description": "For text output: maximum length of the surface-composed mention text." + }, + "max_mention_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "For audio output: maximum duration of the spoken mention in milliseconds." + }, + "supports_offering_reference": { + "type": "boolean", + "description": "Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context." + }, + "supports_landing_page_url": { + "type": "boolean", + "description": "Whether the surface attaches a landing page URL to the mention (citation, learn-more link)." + }, + "tone_constraints": { + "type": "array", + "items": { + "type": "string" + }, + "description": "**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement \u2014 adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance." + }, + "disclosure_required": { + "type": "boolean", + "description": "Whether the surface must include an explicit sponsorship disclosure label." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Custom Format Declaration", + "description": "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 \u2014 kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.", + "properties": { + "format_kind": { + "type": "string", + "const": "custom" + }, + "params": { + "type": "object", + "additionalProperties": true, + "description": "Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`." + } + }, + "required": [ + "format_kind", + "params" + ] + } + ], + "examples": [ + { + "description": "Meta Reels \u2014 narrows video_hosted (vertical orientation)", + "data": { + "format_kind": "video_hosted", + "params": { + "orientation": "vertical", + "aspect_ratio": "9:16", + "duration_ms_range": [ + 3000, + 90000 + ], + "min_width": 1080, + "min_height": 1920, + "max_file_size_mb": 200, + "video_codecs": [ + "h264" + ], + "audio_codecs": [ + "aac" + ], + "headline_max_chars": 25, + "primary_text_max_chars": 72, + "captions": "recommended", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "DOWNLOAD", + "SIGN_UP" + ], + "composition_model": "deterministic", + "platform_extensions": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + } + }, + { + "description": "IAB Medium Rectangle (300x250) \u2014 narrows image", + "data": { + "format_kind": "image", + "params": { + "width": 300, + "height": 250, + "max_file_size_kb": 200, + "image_formats": [ + "jpg", + "png", + "gif" + ], + "ssl_required": true, + "composition_model": "deterministic", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "GET_OFFER" + ] + } + } + }, + { + "description": "Podcast 30s host-read \u2014 narrows audio_hosted with a `script` slot the seller's host reads verbatim. No separate `inputs` map; the script lives in the manifest's `assets` like any other text asset.", + "data": { + "format_kind": "audio_hosted", + "params": { + "duration_ms_exact": 30000, + "audio_codecs": [ + "mp3", + "aac" + ], + "audio_sample_rates": [ + 44100, + 48000 + ], + "audio_channels": [ + "stereo" + ], + "loudness_lufs": -16, + "asset_source": "publisher_host_recorded", + "buyer_asset_acceptance": "rejected", + "composition_model": "deterministic", + "slots": [ + { + "asset_group_id": "script", + "required": true, + "asset_type": "text", + "max_chars": 800 + }, + { + "asset_group_id": "offering_ref", + "required": false, + "asset_type": "text" + } + ], + "production_window_business_days": 7 + } + } + }, + { + "description": "NYTimes Homepage Takeover \u2014 custom format_kind, classified against the multi_placement_takeover format_shape, with format_schema pointing at NYTimes's hosted schema. Buyer agents fetch the schema by uri@digest (cached, immutable) and validate the manifest structurally. `canonical_formats_only: true` is required for custom declarations \u2014 no v1 named format can express the multi-placement shape.", + "data": { + "format_kind": "custom", + "canonical_formats_only": true, + "format_shape": "multi_placement_takeover", + "format_schema": { + "uri": "https://nytimes.example/schemas/formats/homepage_takeover_v3", + "digest": "sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3" + }, + "format_option_id": "nytimes_homepage_takeover_premium", + "display_name": "Homepage Takeover \u2014 Premium Sponsorship", + "applies_to_channels": [ + "display", + "olv" + ], + "params": { + "components": [ + { + "placement_type": "homepage_skin", + "required": true + }, + { + "placement_type": "preroll_video", + "required": true + }, + { + "placement_type": "sponsorship_lockup", + "required": true + } + ], + "exclusivity_window_hours": 24, + "ssl_required": true + } + } + } + ] + } + }, + "placements": { + "type": "array", + "description": "Optional array of specific public placements within this product. Placement IDs are scoped by publisher domain. Product placements declare `kind` to distinguish publisher-referenced placements (`publisher_ref`) from seller-defined inline placements (`seller_inline`). Publisher-referenced placements carry `publisher_domain` plus `placement_id` and may omit `name` because buyers resolve the name from the publisher's adagents.json placement declarations. Seller-inline placements carry buyer-facing `name` directly; when `publisher_domain` is omitted, buyers MAY interpret the placement ID relative to the seller agent's own publisher domain only during the legacy single-publisher transition. Community-maintained fallback files are resolver/source metadata, not a distinct placement kind. Each placement MUST declare `mode: 'targetable'` (buyer may select the placement by PlacementRef, for example in creative assignments) or `mode: 'included'` (part of the public product composition but not buyer-selectable). Placement-level format declarations narrow the product-level creative contract and MUST NOT broaden it. Seller-private delivery objects, source/origin details, and ad-server mappings MUST NOT be exposed here.", + "items": { + "title": "Placement", + "description": "Represents a specific public ad placement within a product's inventory. Placement IDs are scoped by publisher domain, matching placement definitions in that publisher's adagents.json. `kind` is the structural discriminator: `publisher_ref` means this product placement is a reference to `{publisher_domain, placement_id}`; `seller_inline` means the seller is defining public buyer-facing placement metadata inline. The schema accepts either `name` or `publisher_domain` because publisher-referenced placements can omit `name` only when the publisher declaration supplies it; seller-inline placements carry `name` directly. Whether a reference was resolved from publisher-hosted adagents.json or a community-maintained fallback is resolver metadata, not placement structure. Buyers reference placements in creative assignments with structured PlacementRef objects (`publisher_domain` + `placement_id`) when a product spans multiple publishers or the namespace is otherwise ambiguous. Reusing a registered placement preserves the registry's semantic identity; product-level placement objects may narrow format_ids/format_options or add operational detail, but SHOULD NOT redefine the placement's meaning incompatibly.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "publisher_ref", + "seller_inline" + ], + "description": "Placement structure discriminator. `publisher_ref` identifies a placement by `{publisher_domain, placement_id}` and resolves public metadata from the named publisher's adagents.json placement declarations; `seller_inline` identifies buyer-facing placement metadata defined inline by the sales agent (still in the named publisher namespace when `publisher_domain` is present, or the seller's own namespace in legacy single-publisher contexts)." + }, + "placement_id": { + "type": "string", + "description": "Placement identifier in the publisher namespace. When `publisher_domain` is present, this matches a placement ID in that publisher's adagents.json catalog or a seller-defined inline placement in that publisher namespace. Buyers use this with `publisher_domain` in `creative_assignments[].placement_refs`; legacy `creative_assignments[].placement_ids` strings are only unambiguous in single-publisher contexts." + }, + "publisher_domain": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "description": "Publisher domain whose adagents.json placement declarations define this placement. Required for `kind: \"publisher_ref\"`. Omitted only for `kind: \"seller_inline\"` in legacy single-publisher seller contexts where the seller agent's own publisher domain is the namespace." + }, + "name": { + "type": "string", + "description": "Human-readable name for the placement (e.g., 'Homepage Banner', 'Article Sidebar'). Required for `kind: \"seller_inline\"`. May be omitted for publisher-referenced placements because buyers resolve the name from the publisher declaration identified by `{publisher_domain, placement_id}`." + }, + "description": { + "type": "string", + "description": "Detailed description of where and how the placement appears" + }, + "mode": { + "type": "string", + "enum": [ + "targetable", + "included" + ], + "description": "Required product-level relationship to this placement. `targetable` means the buyer may reference this placement_id when assigning creatives or otherwise selecting placements within the product. `included` means the placement is part of the product's public delivery composition but the buyer cannot cherry-pick it by placement_id. During the migration window ending 2026-11-25, buyers MAY tolerate legacy products that omit `mode` and treat them as targetable; after that date buyers SHOULD fail closed. Seller-private delivery objects MUST NOT be exposed here; keep those mappings in seller-internal systems." + }, + "tags": { + "type": "array", + "description": "Optional tags for grouping placements within a product (e.g., 'homepage', 'native', 'premium'). When the placement_id comes from the publisher registry, these should align with the registry tags unless the product is narrowing scope.", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "format_ids": { + "type": "array", + "description": "Format IDs supported by this specific placement. Can include: (1) concrete format_ids (fixed dimensions), (2) template format_ids without parameters (accepts any dimensions/duration), or (3) parameterized format_ids (specific dimension/duration constraints). When present on a product placement, this field narrows the product-level `format_ids` contract for this placement and MUST NOT introduce formats the product does not accept.", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "minItems": 1 + }, + "format_options": { + "type": "array", + "description": "3.1+ canonical format-option declarations supported by this specific product placement. When present, this field narrows the product-level `format_options` contract for this placement and MUST NOT introduce formats the product does not accept. Buyers compute the effective accepted formats for a placement as the intersection of product-level and placement-level declarations; placements without a format declaration inherit the product-level formats.", + "items": { + "title": "Product Format Declaration", + "description": "Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, 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 \u2014 lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). v1 named formats remain supported through the deprecation cycle; v2 product-bound declarations are opt-in.\n\n**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 \u2014 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**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.", + "type": "object", + "required": [ + "format_kind", + "params" + ], + "discriminator": { + "propertyName": "format_kind" + }, + "properties": { + "format_option_id": { + "type": "string", + "description": "Stable identifier for this format declaration within its namespace. REQUIRED when the parent product's `format_options` contains multiple declarations sharing the same `format_kind` (so buyers can disambiguate which option a manifest targets via `manifest.format_option_ref`). SHOULD be set on EVERY `format_options[]` entry \u2014 not just when structurally required to break a `format_kind` collision \u2014 so V2-mental-model buyers can use the V2 authoring path (`PackageRequest.format_option_refs[]`, `creative-manifest.format_option_ref`) against the product. Publisher-catalog-backed options pair this with `publisher_domain`; product-local options omit `publisher_domain` and are selected by `format_option_id` within the target product. A product that ships without selectable `format_option_id` values on its `format_options[]` entries is structurally 3.1-conformant but is not V2-authorable: buyers fall back to v1 `format_ids[]` and lose the stable naming the V2 path was designed to provide. Sellers MUST reject V2 authoring against such products with `UNSUPPORTED_FEATURE` and `error.details.reason` set to `format_option_refs_not_published` per `package-request.json`. Format-internal (not a URI). Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + }, + "publisher_domain": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "description": "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." + }, + "display_name": { + "type": "string", + "description": "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 \u2014 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." + }, + "applies_to_channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "uniqueItems": true, + "description": "Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel \u2014 `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`." + }, + "seller_preference": { + "type": "string", + "enum": [ + "preferred", + "accepted", + "discouraged" + ], + "description": "Optional soft routing hint *within* a product's accepted set of formats \u2014 NOT an enforcement axis. `preferred` \u2014 seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` \u2014 supported on equal footing with other format_options (default when omitted); `discouraged` \u2014 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 \u2014 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." + }, + "canonical_formats_only": { + "type": "boolean", + "default": false, + "description": "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 \u2014 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` \u2014 explicit v2-only is more useful than silent absence." + }, + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, THIS seller's specific product declaration may not work as declared \u2014 even if the underlying canonical is stable. Use for beta runtime paths, forward-looking catalog entries the runtime doesn't yet honor, or experimental products where the seller wants buyer-side caution. Buyers reading `experimental: true` on a product declaration SHOULD prefer the legacy named-format path when a fallback exists for the same product (via `format_ids` on the parent product or via this declaration's `v1_format_ref`) and SHOULD validate via `validate_input` or a sandbox before routing production budget.\n\nIndependent of the canonical's own `experimental` flag \u2014 a stable canonical (e.g., `image`, `video_hosted`) can carry an experimental product declaration when the seller is shipping a new runtime path that isn't fully wired yet. Conversely, an experimental canonical (`sponsored_placement`, `responsive_creative`, `agent_placement`) MAY carry non-experimental product declarations where the seller's adopter contract is well-tested. Buyer SDKs SHOULD filter products with `experimental: true` from default views and offer an opt-in flag to surface them.\n\nReplaces the earlier `runtime_status` enum (`stable | preview | declared_only`) \u2014 same semantic ('use with caution') without the cognitive overhead of two stability axes." + }, + "format_shape": { + "type": "string", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, \u2026). Non-canonical values valid (validators MAY soft-warn) \u2014 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." + }, + "v1_format_ref": { + "type": "array", + "minItems": 1, + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "description": "Authoritative v2 \u2192 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 \u2014 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` \u2014 see the 'Narrows \u2014 formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` \u2287 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 \u2014 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 \u2014 opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` \u2014 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 \u2192 registry glob \u2192 structural match \u2192 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 \u2192 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 \u2026) and the v1 wire fragments into per-publisher namespaces \u2014 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 \u2014 `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 \u22651 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/` \u2014 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." + }, + "format_schema": { + "title": "Platform Extension Reference", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://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 \u2014 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)** \u2014 `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 \u2014 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 \u2014 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 \u22645 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient \u2014 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** \u2014 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 \u00a76 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 \u22648 AND `$ref` count \u2264256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes \u2014 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 \u226410 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget \u2264250 ms (exceeded budget \u2192 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.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "format_kind": { + "const": "custom" + } + }, + "required": [ + "format_kind" + ] + }, + "then": { + "required": [ + "format_shape", + "format_schema" + ], + "anyOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + }, + "else": { + "not": { + "anyOf": [ + { + "required": [ + "format_shape" + ] + }, + { + "required": [ + "format_schema" + ] + } + ] + } + } + }, + { + "$comment": "canonical_formats_only:true and v1_format_ref are mutually exclusive \u2014 a declaration EITHER asserts no v1 projection OR links to a v1 named format, never both.", + "not": { + "allOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + } + }, + { + "$comment": "Canonical-format product declarations use format_option_id; capability_id belongs only to creative-agent build capabilities.", + "not": { + "required": [ + "capability_id" + ] + } + } + ], + "oneOf": [ + { + "title": "Image Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image" + }, + "params": { + "title": "Canonical Format: Image", + "description": "Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters \u2014 covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "image_main", + "asset_type": "image", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image canonical. Buyer ships an image asset (file or hosted URL) plus optional headline, body text, primary text (long-form caption), CTA (typically constrained to an enum via `cta_values`), and clickthrough URL. Products MAY override the default \u2014 make `headline` required, narrow `cta` to a value enum, or remove slots the surface doesn't consume." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required image width in pixels \u2014 use for fixed-size slots (e.g., a 300\u00d7250 IAB MREC). For multi-size flexible slots (publisher MREC slot that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250), use `sizes[]` instead; for responsive slots that adapt to viewport, use `min_width`/`max_width`/`min_height`/`max_height`. The three modes are mutually exclusive \u2014 set exactly one of `(width+height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required image height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. Buyer ships an asset matching one of the listed sizes; SDK validates `assets.image_main.{width,height}` against the list (any-match). Mirrors OpenRTB `banner.format[]` semantics \u2014 one declaration with N accepted sizes is cleaner than N format_options entries. Mutually exclusive with `(width, height)` and with `min/max_width` + `min/max_height` ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width in pixels for responsive slots that adapt within a range (e.g., 'any width from 300 to 970'). Use with `max_width` (and optionally `min_height`/`max_height`). Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width in pixels for responsive slots. Pair with `min_width`. See `min_width` for size-mode mutual exclusion." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height in pixels for responsive slots. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height in pixels for responsive slots. Pair with `min_height`." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside `width`/`height`, must agree. When used with `sizes[]` or responsive ranges, narrows accepted entries to those matching the aspect ratio." + }, + "max_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "svg" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the image and its trackers must be served over HTTPS." + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across all canonicals (`image`, `video_hosted`, `audio_hosted` \u2014 replaces the earlier per-canonical `image_source` / `video_source` / `audio_source` fields). `buyer_uploaded` (default): buyer ships a pre-rendered asset. `publisher_host_recorded`: publisher's host records the asset (audio-specific; podcast host-read pattern). `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE asset at sync_creatives or build_creative time (generative-DSP pattern). `seller_human_designed`: seller's design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class).\n\nNot every value is meaningful on every canonical \u2014 `publisher_host_recorded` is audio-specific; on `image` or `video_hosted` it has no defined behavior. Adopters MUST select a value appropriate to the canonical's asset type. The `slots` declaration is the binding contract for what the buyer ships; `asset_source` is informational and lets buyers understand the production model when picking products." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces 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`)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "HTML5 Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "html5" + }, + "params": { + "title": "Canonical Format: HTML5 Banner", + "description": "Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "html5_bundle", + "asset_type": "zip", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for html5 canonical. Buyer ships a zip bundle plus optional backup image (required when `backup_image_required: true`) and clickthrough URL. The zip's entry point is typically `index.html`; click handling uses the `clickTag` (or `clickTAG`) macro substituted by the seller at serve time." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required banner width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required banner height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot (publisher banner that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250). Mirrors OpenRTB `banner.format[]`. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive HTML5 banners that adapt within a range. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive HTML5 banners. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive HTML5 banners. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive HTML5 banners. Pair with `min_height`." + }, + "max_initial_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile." + }, + "max_polite_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes." + }, + "host_initiated_subload": { + "type": "boolean", + "description": "Whether the host page must initiate the polite-load phase. IAB-compliant banners require true." + }, + "max_animation_duration_ms": { + "type": "integer", + "minimum": 0, + "description": "Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds)." + }, + "max_cpu_load_percent": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Maximum CPU load percentage during render." + }, + "mraid_required": { + "type": "boolean", + "description": "Whether MRAID compatibility is required (mobile in-app)." + }, + "mraid_version": { + "type": "string", + "enum": [ + "2.0", + "3.0" + ], + "description": "Required MRAID version when mraid_required is true." + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether IAB Open Measurement SDK integration is required." + }, + "clicktag_macro": { + "type": "string", + "enum": [ + "clickTag", + "clickTAG" + ], + "description": "Name of the click-tag macro the bundle must use." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the zip for non-HTML5 environments." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum backup image file size in kilobytes." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Display Tag Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "display_tag" + }, + "params": { + "title": "Canonical Format: Display Tag", + "description": "Third-party-served display tag (JS, iframe, or 1\u00d71 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 \u2014 third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "tag_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + } + ], + "description": "Default slots for display_tag canonical. Buyer ships a URL pointing at the third-party-served creative (JS, iframe, or 1\u00d71 redirect) plus an optional backup image. Click and impression macros are substituted into the tag URL by the seller using `universal_macros`." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. The buyer's third-party tag must render at one of the listed sizes; the seller picks which size to request at impression time. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive third-party tags. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive third-party tags. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive third-party tags. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive third-party tags. Pair with `min_height`." + }, + "supported_tag_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "iframe", + "javascript", + "1x1_redirect" + ] + }, + "description": "Tag delivery mechanisms accepted." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the tag URL must be HTTPS." + }, + "max_redirect_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum redirect chain depth permitted." + }, + "max_response_time_ms": { + "type": "integer", + "minimum": 1, + "description": "Maximum tag-server response time in milliseconds." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the tag for environments that cannot render the third-party tag." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1 + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Image Carousel Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image_carousel" + }, + "params": { + "title": "Canonical Format: Image Carousel", + "description": "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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 multi-card carousels (Meta carousel, Pinterest pin collections, Snap collection ads) weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "cards", + "asset_type": "card", + "required": true, + "min": 2, + "max": 10 + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image_carousel. The `cards` slot's value in the manifest is an array of [card-asset](/schemas/core/assets/card-asset.json) objects; `min` / `max` constrain card count." + }, + "card_aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5')." + }, + "min_cards": { + "type": "integer", + "minimum": 2, + "description": "Minimum card count (typical: 2 or 3)." + }, + "max_cards": { + "type": "integer", + "description": "Maximum card count (typical: 6, 10, or 35 depending on platform)." + }, + "allowed_card_media_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "Asset types each card's `media` field may carry. Default: ['image']. Polymorphic carousels (Meta) allow ['image', 'video']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card's media payload, not the card-asset itself (which is always asset_type: \"card\")." + }, + "allowed_card_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "DEPRECATED \u2014 alias for `allowed_card_media_asset_types`. Kept for back-compat; prefer the new field name. Removed in 5.0." + }, + "card_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "card_video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum length of the carousel-level primary text." + }, + "card_headline_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card headline character limit. Governs the `headline` field on each card-asset in the `cards` slot." + }, + "card_description_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars)." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Video", + "description": "Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) \u2014 receivers fire impression and click pixels at delivery time.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "video_main", + "asset_type": "video", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "companion_banner", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_hosted canonical. Buyer ships a video asset (file or hosted URL); optional headline, primary text (long-form caption), CTA (typically constrained via `cta_values`), brand_name (typical for vertical short-form), companion_banner (typical for horizontal instream), and clickthrough URL. Products MAY override or extend the default \u2014 e.g., remove `companion_banner` for short-form vertical, narrow `cta` to a value enum, mark `landing_page_url` as required." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ], + "description": "Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed)." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio. Inferred from orientation if omitted." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: when both `duration_ms_exact` and `duration_ms_range` ship on the same product, `duration_ms_exact` takes precedence \u2014 buyers MUST validate against the exact value and ignore the range. The range is treated as advisory metadata in that case (e.g., for UI display showing the broader product family). SDKs SHOULD lint a warning when both fields ship; producers SHOULD pick one." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship (see `duration_ms_range` description)." + }, + "video_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "h264", + "h265", + "vp8", + "vp9", + "av1", + "prores" + ] + } + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aac", + "mp3", + "opus", + "pcm" + ] + } + }, + "containers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp4", + "webm", + "mov" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_file_size_mb": { + "type": "integer", + "minimum": 1 + }, + "frame_rates": { + "type": "array", + "items": { + "type": "number" + } + }, + "captions": { + "type": "string", + "enum": [ + "required", + "recommended", + "not_required" + ] + }, + "om_sdk_required": { + "type": "boolean" + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + } + }, + "companion_banner_widths": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + }, + "description": "Permitted companion banner widths (instream video)." + }, + "companion_banner_heights": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across canonicals. See `image.json#asset_source` for the full semantics. `publisher_host_recorded` is audio-specific and has no defined behavior on video \u2014 adopters MUST select a value appropriate to the canonical." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the video." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "VAST Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_vast" + }, + "params": { + "title": "Canonical Format: VAST Video", + "description": "VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "vast_tag", + "asset_type": "vast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_vast canonical. Buyer ships a VAST tag (URL or inline XML, VAST 2.x-4.x) plus an optional clickthrough URL (which falls back to the VAST `ClickThrough` element when omitted). Tracking events are inherent to VAST and don't require explicit slots." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ] + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$" + }, + "vast_version": { + "type": "string", + "enum": [ + "2.0", + "3.0", + "4.0", + "4.1", + "4.2" + ], + "description": "Required VAST version." + }, + "vpaid_enabled": { + "type": "boolean", + "description": "Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads." + }, + "vpaid_version": { + "type": "string", + "enum": [ + "1.0", + "2.0" + ] + }, + "simid_supported": { + "type": "boolean", + "description": "Whether IAB SIMID interactive video extensions are supported." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "linear_required": { + "type": "boolean", + "description": "Whether the VAST creative must be linear (non-skippable in-stream)." + }, + "skippable_after_ms": { + "type": "integer", + "minimum": 0, + "description": "When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll)." + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum VAST wrapper redirect depth permitted." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Audio", + "description": "Direct audio creative \u2014 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) \u2014 there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "audio_main", + "asset_type": "audio", + "required": true + }, + { + "asset_group_id": "companion_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for buyer-uploaded audio. Host-read products override with a `script` (asset_type: text) or `creative_brief` (asset_type: brief) slot in place of `audio_main`, plus `asset_source: 'publisher_host_recorded'` and `buyer_asset_acceptance: 'rejected'`. TTS-from-script products override similarly with `asset_source: 'seller_pre_rendered_from_brief'`." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship on the same product. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp3", + "aac", + "wav", + "opus", + "flac" + ] + } + }, + "audio_sample_rates": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "audio_channels": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mono", + "stereo" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "loudness_lufs": { + "type": "number", + "description": "Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values." + }, + "loudness_tolerance_db": { + "type": "number", + "minimum": 0, + "description": "Permitted deviation from loudness_lufs in dB." + }, + "true_peak_dbfs": { + "type": "number", + "description": "Maximum true-peak level in dBFS (typical: -2)." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered audio bytes come from. Single shared enum across canonicals (see `image.json#asset_source` for the full semantics). `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. This value is audio-specific." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `asset_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`)." + }, + "companion_image_required": { + "type": "boolean" + }, + "companion_image_aspect_ratio": { + "type": "string" + }, + "companion_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "DAAST Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_daast" + }, + "params": { + "title": "Canonical Format: DAAST Audio", + "description": "DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "daast_tag", + "asset_type": "daast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for audio_daast canonical. Buyer ships a DAAST tag (URL or inline XML, 1.0 or 1.1) plus an optional clickthrough URL. Tracking events are inherent to DAAST and don't require explicit slots." + }, + "daast_version": { + "type": "string", + "enum": [ + "1.0", + "1.1" + ] + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "linear_required": { + "type": "boolean" + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0 + }, + "ssl_required": { + "type": "boolean" + }, + "companion_image_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Sponsored Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "sponsored_placement" + }, + "params": { + "title": "Canonical Format: Sponsored Placement (retail-media catalog-driven)", + "description": "Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset \u2014 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** \u2014 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 \u2014 buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot \u2014 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) \u2014 use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Single-image or single-video creative \u2014 use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing \u2014 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).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical covers 4 meaningfully different retail-media adapter contracts (Amazon SP, Criteo SP / CitrusAd SP, Pinterest Collection, generative-per-SKU). Adopter contracts vary; buyers MUST validate per-adapter behavior before routing budget. Promotion to non-experimental gated on the #4592 adapter-contract docs work." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 retail-media catalog placements weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural, not a registry-coverage gap." + }, + "slots": { + "default": [ + { + "asset_group_id": "source_catalog", + "required": true, + "asset_type": "catalog" + }, + { + "asset_group_id": "hero_asset", + "required": false, + "asset_type": "image" + }, + { + "asset_group_id": "landing_page_url", + "required": false, + "asset_type": "url" + } + ] + }, + "supported_catalog_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "product", + "store", + "offering", + "hotel", + "flight", + "vehicle", + "real_estate", + "education", + "destination", + "app", + "job", + "inventory" + ] + }, + "description": "Catalog types this product accepts." + }, + "min_items": { + "type": "integer", + "minimum": 1, + "description": "Minimum catalog item count buyer must supply." + }, + "max_items": { + "type": "integer", + "description": "Maximum items considered for placement." + }, + "fanout_mode": { + "type": "string", + "enum": [ + "per_item", + "multi_item_in_creative", + "single_item" + ], + "description": "How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item." + }, + "required_catalog_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])." + }, + "supported_id_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "asin", + "sku", + "gtin", + "offering_id", + "store_id", + "hotel_id", + "flight_id", + "vehicle_id", + "listing_id", + "program_id", + "destination_id", + "app_id", + "job_id" + ] + }, + "description": "Catalog identifier types the placement renders against." + }, + "hero_asset_supported": { + "type": "boolean", + "description": "Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern)." + }, + "item_production_model": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "How each per-item creative is produced. Covers the same production-source axis as `asset_source` on `image` / `video_hosted` / `audio_hosted` but with a 4-value subset \u2014 drops `publisher_host_recorded` because it's audio-specific and doesn't apply to retail-media catalog placements. SDK codegen MAY share a base enum and narrow per-canonical, or emit two distinct enums; either way the wire values overlap exactly for the 4 retained values. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer's catalog already contains rendered assets per item; the seller composes the placement using those assets. (\"Uploaded\" reads slightly off for catalog-keyed items where the buyer didn't actively upload bytes \u2014 the catalog ingestion already supplied them \u2014 but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller's design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief \u00d7 N catalog items \u2192 N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Native In-Feed Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "native_in_feed" + }, + "params": { + "title": "Canonical Format: Native In-Feed", + "description": "IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types \u2014 `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 \u2014 visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 \u2014 app/product rating), `price` (Data Asset type 6 \u2014 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 \u2014 type 4, downloads \u2014 type 5, saleprice \u2014 type 7, phone_number \u2014 type 8, address \u2014 type 9, desc2 \u2014 type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel \u2014 feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative \u2014 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) \u2014 use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Multi-card carousel \u2014 use `image_carousel`.\n- Video-first native units where the asset is a hosted video file \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": false, + "description": "Stable at 3.1 GA. Shape mirrors IAB OpenRTB Native 1.2 \u2014 the renderer contract is well-established across in-feed native and content-recommendation adopters." + }, + "v1_translatable": { + "default": true, + "description": "Translates to v1 named native formats (e.g., `native_standard`, `native_content`) via the projection registry. Sellers with existing v1 named native formats SHOULD point `v1_format_ref[]` at them." + }, + "slots": { + "default": [ + { + "asset_group_id": "title", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "main_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "icon", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "advertiser_name", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "sponsored_label", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "display_url", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "rating", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "price", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "impression_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "viewability_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "click_tracker", + "asset_type": "pixel_tracker", + "required": false + } + ], + "description": "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)." + }, + "title_max_chars": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the body_text slot. IAB native typical: 90 (mainline) to 140 (extended)." + }, + "cta_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the cta slot. Typical: 15\u201325." + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD']). When set, narrows the cta slot to a closed enum." + }, + "main_image_sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "Accepted (width, height) pairs for the main_image slot. Common IAB native sizes: 1200\u00d7627 (1.91:1), 1080\u00d71080 (1:1), 1080\u00d71350 (4:5)." + }, + "icon_size": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false, + "description": "Required (width, height) for the icon slot when present (typical: 80\u00d780 or 100\u00d7100)." + }, + "max_image_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes for main_image and icon." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether trackers, landing pages, and image URLs must be served over HTTPS." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered native assets come from. `publisher_host_recorded` is omitted (audio-specific and not meaningful for native). Other values mirror the shared production-source axis used on `image` / `video_hosted`. `buyer_uploaded` (default): buyer ships pre-rendered title/image/body. `seller_pre_rendered_from_brief`: buyer ships a brief, seller renders the native bundle. `agent_synthesized`: AI synthesis pipeline produces title + image + body from a brief; pair with `synthesis_nondeterministic: true` for generative pipelines that can't guarantee in-spec output." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded native assets. When `rejected`, the buyer cannot ship pre-rendered title/image/body \u2014 they must use `build_creative` (or `sync_creatives` with brief inputs) so the seller produces the native bundle from a brief." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Responsive Creative Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "responsive_creative" + }, + "params": { + "title": "Canonical Format: Responsive Creative", + "description": "Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** \u2014 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.).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: composition is algorithmic (the surface picks combinations and reports per-asset breakdowns), and there's no clean v1-translatable equivalent. Buyers ship asset pools rather than rendered creatives; the surface's per-impression composition cannot be predicted by `validate_input`. Adopters SHOULD validate behavior per surface (Google PMax vs Meta Advantage+ creative differ meaningfully)." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 algorithmic asset-pool composition (Google PMax / Meta Advantage+ creative) wasn't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "headlines", + "asset_type": "text", + "required": true, + "min": 3, + "max": 15 + }, + { + "asset_group_id": "long_headlines", + "asset_type": "text", + "required": false, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "descriptions", + "asset_type": "text", + "required": true, + "min": 2, + "max": 5 + }, + { + "asset_group_id": "images_landscape", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_square", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_vertical", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "video", + "asset_type": "video", + "required": false, + "min": 0, + "max": 5 + }, + { + "asset_group_id": "logo", + "asset_type": "image", + "required": true, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true, + "min": 1, + "max": 1 + } + ] + }, + "headlines_min": { + "type": "integer", + "minimum": 0 + }, + "headlines_max": { + "type": "integer", + "minimum": 0 + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "long_headlines_min": { + "type": "integer", + "minimum": 0 + }, + "long_headlines_max": { + "type": "integer", + "minimum": 0 + }, + "long_headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "descriptions_min": { + "type": "integer", + "minimum": 0 + }, + "descriptions_max": { + "type": "integer", + "minimum": 0 + }, + "description_max_chars": { + "type": "integer", + "minimum": 1 + }, + "images_landscape_min": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_max": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_aspect_ratio": { + "type": "string" + }, + "images_square_min": { + "type": "integer", + "minimum": 0 + }, + "images_square_max": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_min": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_max": { + "type": "integer", + "minimum": 0 + }, + "videos_min": { + "type": "integer", + "minimum": 0 + }, + "videos_max": { + "type": "integer", + "minimum": 0 + }, + "video_min_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "logo_min": { + "type": "integer", + "minimum": 0 + }, + "logo_max": { + "type": "integer", + "minimum": 0 + }, + "logo_aspect_ratios": { + "type": "array", + "items": { + "type": "string" + } + }, + "business_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "asset_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "supports_catalog_input": { + "type": "boolean", + "description": "Whether the product can additionally consume a catalog reference (e.g., PMax with product feed)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Agent Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "agent_placement" + }, + "params": { + "title": "Canonical Format: Agent Placement (AI-surface sponsored placement)", + "description": "**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 set `runtime_status: 'preview'` or `'declared_only'` on the declaration; 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** \u2014 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 \u2014 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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical's tracking model (mention-level impression + attribution, postback shape, cross-surface dedup) is intentionally underspecified for 3.1. Adopters claiming `agent_placement` ship private tracking integrations; buyer agents MUST treat attribution as adapter-defined until the 3.2 tracking-macro spec lands. Promotion to non-experimental gated on the 3.2 tracking-contract spec." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 AI-surface sponsored mentions weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "offering_ref", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "agent_placement has minimal buyer-shipped slots \u2014 the surface composes the rendered output from brand context (resolved via the manifest's top-level `brand` BrandRef) plus optional offering_ref and landing_page_url assets. None of these assets are rendered verbatim by the buyer; the agent chooses how to use them." + }, + "output_modality": { + "type": "string", + "enum": [ + "text", + "audio", + "card" + ], + "description": "How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text." + }, + "max_mention_length_chars": { + "type": "integer", + "minimum": 1, + "description": "For text output: maximum length of the surface-composed mention text." + }, + "max_mention_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "For audio output: maximum duration of the spoken mention in milliseconds." + }, + "supports_offering_reference": { + "type": "boolean", + "description": "Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context." + }, + "supports_landing_page_url": { + "type": "boolean", + "description": "Whether the surface attaches a landing page URL to the mention (citation, learn-more link)." + }, + "tone_constraints": { + "type": "array", + "items": { + "type": "string" + }, + "description": "**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement \u2014 adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance." + }, + "disclosure_required": { + "type": "boolean", + "description": "Whether the surface must include an explicit sponsorship disclosure label." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Custom Format Declaration", + "description": "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 \u2014 kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.", + "properties": { + "format_kind": { + "type": "string", + "const": "custom" + }, + "params": { + "type": "object", + "additionalProperties": true, + "description": "Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`." + } + }, + "required": [ + "format_kind", + "params" + ] + } + ], + "examples": [ + { + "description": "Meta Reels \u2014 narrows video_hosted (vertical orientation)", + "data": { + "format_kind": "video_hosted", + "params": { + "orientation": "vertical", + "aspect_ratio": "9:16", + "duration_ms_range": [ + 3000, + 90000 + ], + "min_width": 1080, + "min_height": 1920, + "max_file_size_mb": 200, + "video_codecs": [ + "h264" + ], + "audio_codecs": [ + "aac" + ], + "headline_max_chars": 25, + "primary_text_max_chars": 72, + "captions": "recommended", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "DOWNLOAD", + "SIGN_UP" + ], + "composition_model": "deterministic", + "platform_extensions": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + } + }, + { + "description": "IAB Medium Rectangle (300x250) \u2014 narrows image", + "data": { + "format_kind": "image", + "params": { + "width": 300, + "height": 250, + "max_file_size_kb": 200, + "image_formats": [ + "jpg", + "png", + "gif" + ], + "ssl_required": true, + "composition_model": "deterministic", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "GET_OFFER" + ] + } + } + }, + { + "description": "Podcast 30s host-read \u2014 narrows audio_hosted with a `script` slot the seller's host reads verbatim. No separate `inputs` map; the script lives in the manifest's `assets` like any other text asset.", + "data": { + "format_kind": "audio_hosted", + "params": { + "duration_ms_exact": 30000, + "audio_codecs": [ + "mp3", + "aac" + ], + "audio_sample_rates": [ + 44100, + 48000 + ], + "audio_channels": [ + "stereo" + ], + "loudness_lufs": -16, + "asset_source": "publisher_host_recorded", + "buyer_asset_acceptance": "rejected", + "composition_model": "deterministic", + "slots": [ + { + "asset_group_id": "script", + "required": true, + "asset_type": "text", + "max_chars": 800 + }, + { + "asset_group_id": "offering_ref", + "required": false, + "asset_type": "text" + } + ], + "production_window_business_days": 7 + } + } + }, + { + "description": "NYTimes Homepage Takeover \u2014 custom format_kind, classified against the multi_placement_takeover format_shape, with format_schema pointing at NYTimes's hosted schema. Buyer agents fetch the schema by uri@digest (cached, immutable) and validate the manifest structurally. `canonical_formats_only: true` is required for custom declarations \u2014 no v1 named format can express the multi-placement shape.", + "data": { + "format_kind": "custom", + "canonical_formats_only": true, + "format_shape": "multi_placement_takeover", + "format_schema": { + "uri": "https://nytimes.example/schemas/formats/homepage_takeover_v3", + "digest": "sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3" + }, + "format_option_id": "nytimes_homepage_takeover_premium", + "display_name": "Homepage Takeover \u2014 Premium Sponsorship", + "applies_to_channels": [ + "display", + "olv" + ], + "params": { + "components": [ + { + "placement_type": "homepage_skin", + "required": true + }, + { + "placement_type": "preroll_video", + "required": true + }, + { + "placement_type": "sponsorship_lockup", + "required": true + } + ], + "exclusivity_window_hours": 24, + "ssl_required": true + } + } + } + ] + }, + "minItems": 1 + } + }, + "required": [ + "kind", + "placement_id", + "mode" + ], + "$comment": "The anyOf(name OR publisher_domain) is a schema-local proxy for the cross-document invariant: publisher-referenced placements can omit name only because {publisher_domain, placement_id} resolves to an adagents.json placement that supplies the name. Seller-inline placements carry name directly.", + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "publisher_domain" + ] + } + ], + "allOf": [ + { + "$comment": "Public product placements must not leak seller-private delivery mapping fields. Consumers that detect this leak should surface PRIVATE_FIELD_IN_PUBLIC_PLACEMENT for monitoring.", + "not": { + "anyOf": [ + { + "required": [ + "visibility" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "origin" + ] + }, + { + "required": [ + "delivery_mappings" + ] + } + ] + } + }, + { + "if": { + "properties": { + "kind": { + "type": "string", + "const": "publisher_ref" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "publisher_domain" + ] + } + }, + { + "if": { + "properties": { + "kind": { + "type": "string", + "const": "seller_inline" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "name" + ] + } + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "delivery_type": { + "$ref": "#/$defs/DeliveryType" + }, + "exclusivity": { + "$ref": "#/$defs/Exclusivity" + }, + "pricing_options": { + "type": "array", + "description": "Available pricing models for this product", + "items": { + "title": "Pricing Option", + "description": "A pricing model option offered by a publisher for a product. Discriminated by pricing_model field. If fixed_price is present, it's fixed pricing. If absent, it's auction-based (floor_price and price_guidance optional). Bid-based auction models may also include max_bid as a boolean signal to interpret bid_price as a buyer ceiling instead of an exact honored price.", + "discriminator": { + "propertyName": "pricing_model" + }, + "oneOf": [ + { + "title": "CPM Pricing Option", + "description": "Cost Per Mille (cost per 1,000 impressions) pricing. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpm", + "description": "Cost per 1,000 impressions" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "vCPM Pricing Option", + "description": "Viewable Cost Per Mille (cost per 1,000 viewable impressions) pricing - MRC viewability standard. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "vcpm", + "description": "Cost per 1,000 viewable impressions (MRC standard)" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CPC Pricing Option", + "description": "Cost Per Click pricing. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpc", + "description": "Cost per click" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per click. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CPCV Pricing Option", + "description": "Cost Per Completed View (100% video/audio completion) pricing. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpcv", + "description": "Cost per completed view (100% completion)" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per completed view. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CPV Pricing Option", + "description": "Cost Per View (at publisher-defined threshold) pricing for video/audio. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpv", + "description": "Cost per view at threshold" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per view. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "type": "object", + "description": "CPV-specific parameters defining the view threshold", + "properties": { + "view_threshold": { + "oneOf": [ + { + "type": "number", + "description": "Percentage completion threshold (0.0 to 1.0, e.g., 0.5 = 50%)", + "minimum": 0, + "maximum": 1 + }, + { + "type": "object", + "description": "Time-based view threshold", + "properties": { + "duration_seconds": { + "type": "integer", + "description": "Seconds of viewing required", + "minimum": 1 + } + }, + "required": [ + "duration_seconds" + ], + "additionalProperties": true + } + ] + } + }, + "required": [ + "view_threshold" + ], + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency", + "parameters" + ], + "additionalProperties": true + }, + { + "title": "CPP Pricing Option", + "description": "Cost Per Point (Gross Rating Point) pricing for TV and audio campaigns. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpp", + "description": "Cost per Gross Rating Point" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per rating point. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "type": "object", + "description": "CPP-specific parameters for demographic targeting", + "properties": { + "demographic_system": { + "$ref": "#/$defs/DemographicSystem" + }, + "demographic": { + "type": "string", + "description": "Target demographic code within the specified demographic_system (e.g., P18-49 for Nielsen, ABC1 Adults for BARB)" + }, + "min_points": { + "type": "number", + "description": "Minimum GRPs/TRPs required", + "minimum": 0 + } + }, + "required": [ + "demographic" + ], + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency", + "parameters" + ], + "additionalProperties": true + }, + { + "title": "CPA Pricing Option", + "description": "Cost Per Acquisition pricing. Advertiser pays a fixed price when a specified conversion event occurs. The event_type field declares which event triggers billing (e.g., purchase, lead, app_install).", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpa", + "description": "Cost per acquisition (conversion event)" + }, + "event_type": { + "allOf": [ + { + "$ref": "#/$defs/EventType" + } + ], + "description": "The conversion event type that triggers billing (e.g., purchase, lead, app_install)" + }, + "custom_event_name": { + "type": "string", + "description": "Name of the custom event when event_type is 'custom'. Required when event_type is 'custom', ignored otherwise." + }, + "event_source_id": { + "type": "string", + "description": "When present, only events from this specific event source count toward billing. Allows different CPA rates for different sources (e.g., online vs in-store purchases). Must match an event source configured via sync_event_sources." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per acquisition in the specified currency", + "exclusiveMinimum": 0 + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "event_type", + "currency", + "fixed_price" + ], + "additionalProperties": true + }, + { + "title": "Flat Rate Pricing Option", + "description": "Flat rate pricing for sponsorships, takeovers, and DOOH exclusive placements. A fixed total cost regardless of delivery volume. For duration-scaled pricing (rate \u00d7 time units), use the `time` model instead. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "flat_rate", + "description": "Fixed cost regardless of delivery volume" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Flat rate cost. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "title": "DoohParameters", + "description": "DOOH inventory allocation parameters. Sponsorship and takeover flat_rate options omit this field entirely \u2014 only include for digital out-of-home inventory.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "dooh", + "description": "Discriminator identifying this as DOOH parameters" + }, + "sov_percentage": { + "type": "number", + "description": "Guaranteed share of voice as a percentage (0-100)", + "minimum": 0, + "maximum": 100 + }, + "loop_duration_seconds": { + "type": "integer", + "description": "Duration of the ad loop rotation in seconds", + "minimum": 1 + }, + "min_plays_per_hour": { + "type": "integer", + "description": "Minimum number of plays per hour guaranteed", + "minimum": 1 + }, + "venue_package": { + "type": "string", + "description": "Named collection of screens included in this buy" + }, + "duration_hours": { + "type": "number", + "description": "Duration of the DOOH slot in hours (e.g., 24 for a full-day takeover)", + "minimum": 0 + }, + "daypart": { + "type": "string", + "description": "Named daypart for this slot (e.g., morning_commute, evening_rush)" + }, + "estimated_impressions": { + "type": "integer", + "description": "Estimated audience impressions for this slot (informational, not a delivery guarantee)", + "minimum": 0 + } + }, + "required": [ + "type" + ], + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "Time-Based Pricing Option", + "description": "Cost per time unit (hour, day, week, or month) - rate scales with campaign duration. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "time", + "description": "Cost per time unit - rate scales with campaign duration" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Cost per time unit. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid per time unit for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "type": "object", + "description": "Time-based pricing parameters", + "required": [ + "time_unit" + ], + "properties": { + "time_unit": { + "type": "string", + "enum": [ + "hour", + "day", + "week", + "month" + ], + "description": "The time unit for pricing. Total cost = fixed_price \u00d7 number of time_units in the campaign flight." + }, + "min_duration": { + "type": "integer", + "minimum": 1, + "description": "Minimum booking duration in time_units" + }, + "max_duration": { + "type": "integer", + "minimum": 1, + "description": "Maximum booking duration in time_units. Must be >= min_duration when both are present." + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency", + "parameters" + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + }, + "forecast": { + "title": "Delivery Forecast", + "description": "Forecasted delivery metrics for this product. Gives buyers an estimate of expected performance before requesting a proposal.", + "type": "object", + "properties": { + "points": { + "type": "array", + "description": "Forecasted delivery data points. For spend curves (default), points at ascending budget levels show how metrics scale with spend. For availability forecasts, points represent total available inventory independent of budget. See forecast_range_unit for interpretation.", + "items": { + "title": "Forecast Point", + "description": "A forecast data point. When budget is present, the point pairs a spend level with expected delivery \u2014 multiple points at ascending budgets form a curve. When budget is omitted, the point represents total available inventory for the requested targeting and dates, independent of spend.", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 128, + "description": "Human-readable name for this forecast point. Required when forecast_range_unit is 'package' so buyer agents can identify and reference individual packages. Optional for other forecast types.", + "examples": [ + "Primetime", + "Morning Drive", + "Large Format Transit" + ] + }, + "budget": { + "type": "number", + "description": "Budget amount for this forecast point. Required for spend curves; omit for availability forecasts where the metrics represent total available inventory. For allocation-level forecasts, this is the absolute budget for that allocation (not the percentage). For proposal-level forecasts, this is the total proposal budget. When omitted, use metrics.spend to express the estimated cost of the available inventory.", + "minimum": 0 + }, + "metrics": { + "type": "object", + "description": "Forecasted metric values. Keys are forecastable-metric enum values for delivery/engagement or event-type enum values for outcomes. Values are ForecastRange objects (low/mid/high). Use { \"mid\": value } for point estimates. When budget is present, these are the expected metrics at that spend level. When budget is omitted, these represent total available inventory \u2014 use spend to express the estimated cost. Additional keys beyond the documented properties are allowed for event-type values (purchase, lead, app_install, etc.).", + "properties": { + "audience_size": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "reach": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "frequency": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "clicks": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "spend": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "completed_views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "grps": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "engagements": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "follows": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "saves": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "profile_visits": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "measured_impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "downloads": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "plays": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + }, + "additionalProperties": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + } + }, + "required": [ + "metrics" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "forecast_range_unit": { + "$ref": "#/$defs/ForecastRangeUnit" + }, + "method": { + "$ref": "#/$defs/ForecastMethod" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for monetary values in this forecast (spend, budget)" + }, + "demographic_system": { + "$ref": "#/$defs/DemographicSystem" + }, + "demographic": { + "type": "string", + "description": "Target demographic code within the specified demographic_system. For Nielsen: P18-49, M25-54, W35+. For BARB: ABC1 Adults, 16-34. For AGF: E 14-49.", + "examples": [ + "P18-49", + "A25-54", + "W35+", + "M18-34" + ] + }, + "measurement_source": { + "type": "string", + "maxLength": 64, + "pattern": "^[a-z0-9_]+$", + "description": "Third-party measurement provider whose data was used to produce this forecast. Distinct from demographic_system, which specifies demographic notation \u2014 measurement_source identifies whose data produced the forecast numbers. Should be present when measured_impressions is used. Lowercase slug format.", + "examples": [ + "nielsen", + "videoamp", + "comscore", + "geopath", + "barb", + "agf", + "oztam", + "kantar", + "barc", + "route", + "rajar", + "triton" + ] + }, + "reach_unit": { + "$ref": "#/$defs/ReachUnit" + }, + "generated_at": { + "type": "string", + "format": "date-time", + "description": "When this forecast was computed" + }, + "valid_until": { + "type": "string", + "format": "date-time", + "description": "When this forecast expires. After this time, the forecast should be refreshed. Forecast expiry does not affect proposal executability." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "points", + "method", + "currency" + ], + "additionalProperties": true + }, + "outcome_measurement": { + "title": "Outcome Measurement (Deprecated)", + "description": "**Deprecated as of this minor.** Outcome capabilities (incremental sales lift, brand lift, foot traffic, etc.) are now declared via `reporting_capabilities.available_metrics` (the same path used for impressions, conversions, ROAS) with `qualifier.attribution_methodology` and `qualifier.attribution_window` carrying the methodology and window on commit. New implementations SHOULD use the unified pattern; this field is retained for one-minor backwards compatibility and removed at the next major. See `outcome-measurement.json` description for migration guidance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of measurement", + "examples": [ + "incremental_sales_lift", + "brand_lift", + "foot_traffic" + ] + }, + "attribution": { + "type": "string", + "description": "Attribution methodology", + "examples": [ + "deterministic_purchase", + "probabilistic" + ] + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Attribution window as a structured duration (e.g., {\"interval\": 30, \"unit\": \"days\"})." + }, + "reporting": { + "type": "string", + "description": "Reporting frequency and format", + "examples": [ + "weekly_dashboard", + "real_time_api" + ] + } + }, + "required": [ + "type", + "attribution", + "reporting" + ], + "additionalProperties": true + }, + "delivery_measurement": { + "type": "object", + "description": "Measurement vendors and methodology for delivery metrics. The buyer accepts the declared vendors as the source of truth for the buy. When absent, buyers should apply their own measurement defaults. Senders SHOULD populate `vendors` (structured BrandRef array) for new implementations; the legacy `provider` string field is deprecated and retained for one-minor backwards compatibility.", + "properties": { + "vendors": { + "type": "array", + "description": "Measurement vendors used for this product, as structured `BrandRef` identities. Multiple entries when multiple vendors play different roles (e.g., the ad server plus a separate viewability vendor like IAS or DV; or a retail-media seller plus a third-party retail measurement vendor like Circana or NielsenIQ). Each vendor's `brand.json` `agents[type='measurement']` is the discovery anchor; metric definitions live on the agent's `get_adcp_capabilities.measurement.metrics[]` block. Distinct from `performance_standards[].vendor` which carries vendor identity for *committed* metrics with thresholds \u2014 this field carries vendor identity for the overall measurement story, including non-committed-but-reported metrics.", + "items": { + "title": "Brand Reference", + "description": "Reference to a brand by domain and optional brand_id. The domain hosts /.well-known/brand.json or is registered in the brand registry. For single-brand domains, brand_id can be omitted. For house-of-brands domains, brand_id identifies the specific brand.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "minItems": 1 + }, + "provider": { + "type": "string", + "description": "**Deprecated as of this minor.** Free-form measurement provider description (e.g., 'Google Ad Manager with IAS viewability', 'Nielsen DAR', 'Geopath for DOOH impressions'). New implementations SHOULD use the structured `vendors` field instead. Retained for one-minor backwards compatibility; removed at the next major. When both `vendors` and `provider` are present, consumers MUST use `vendors` for vendor identity and treat `provider` as informational text." + }, + "notes": { + "type": "string", + "description": "Additional details about measurement methodology in plain language (e.g., 'MRC-accredited viewability. 50% in-view for 1s display / 2s video', 'Panel-based demographic measurement updated monthly'). Free-form prose for context that doesn't fit the structured `vendors` field." + } + } + }, + "measurement_terms": { + "title": "Measurement Terms", + "description": "Seller's default billing measurement and makegood terms. Declares who counts the billing metric and what remedies apply when thresholds are breached. Buyers may propose different terms at media buy creation \u2014 sellers accept, reject (TERMS_REJECTED), or adjust per their policy.", + "type": "object", + "properties": { + "billing_measurement": { + "type": "object", + "description": "Which vendor's count of the billing metric governs invoicing. The billing metric is determined by the pricing_model on the selected pricing_option (e.g., impressions for CPM, completed views for CPCV).", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor whose measurement of the billing metric is authoritative for invoicing (e.g., { domain: 'campaignmanager.google.com' } for buyer's DCM, { domain: 'admanager.google.com' } for seller's GAM).", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "max_variance_percent": { + "type": "number", + "minimum": 0, + "exclusiveMaximum": 100, + "description": "Maximum acceptable variance between the billing vendor's count and the other party's count before resolution is triggered (e.g., 10 means a 10% divergence triggers review)." + }, + "measurement_window": { + "type": "string", + "description": "Which measurement maturation stage the billing metric is reconciled against. References a window_id from the product's reporting_capabilities.measurement_windows. Examples: 'c7' for broadcast TV guarantees (live + 7 days DVR), 'final' for DOOH after IVT/fraud-check processing, 'post_sivt' for digital after sophisticated invalid-traffic filtering, 'downloads_30d' for podcast. When absent, billing is based on the seller's standard reporting without windowed maturation.", + "examples": [ + "live", + "c3", + "c7", + "tentative", + "final", + "post_ivt", + "post_sivt", + "downloads_30d" + ] + }, + "finalization_deadline_hours": { + "type": "integer", + "minimum": 0, + "description": "Maximum hours by which the authoritative party MUST publish a final record (`is_final: true` / `finalized_at` on `get_media_buy_delivery`, or `final: true` / `finalized_at` on `report_usage`). **Anchor:** when `measurement_window` is set, hours are counted from the close of that window (e.g., 240h after `c7` close = ~10 days after the 7-day DVR accumulation completes); when `measurement_window` is absent, hours are counted from `reporting_period.end`. Picking a single anchor avoids ambiguity for windowed channels where `reporting_period.end` and window close differ by days. The deadline applies to whichever party is named in `vendor` \u2014 seller, buyer, or third-party vendor \u2014 symmetrically. When the deadline elapses without a final record, the counterparty MAY fall back to its own attestation for invoicing (seller falls back to seller-attested numbers via `get_media_buy_delivery`; buyer falls back to a buyer-attested `report_usage` push), and the breach is treated like any other measurement-terms breach under `makegood_policy`. Absent means no contractual deadline \u2014 finalization is best-effort and disagreements resolve out of band." + } + }, + "required": [ + "vendor" + ], + "additionalProperties": true + }, + "makegood_policy": { + "type": "object", + "description": "Remedies available when a performance standard or billing measurement variance is breached. Seller declares which remedy types they support. When a breach occurs, the seller proposes a remedy from this menu; the buyer accepts or disputes.", + "properties": { + "available_remedies": { + "type": "array", + "description": "Remedy types the seller supports. Ordered by seller preference (first = preferred). Seller proposes from this list when a breach occurs; buyer accepts or disputes.", + "items": { + "$ref": "#/$defs/MakegoodRemedy" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "available_remedies" + ], + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "performance_standards": { + "type": "array", + "description": "Seller's default performance standards for this product: viewability, IVT, completion rate, brand safety, attention score. Buyers may propose different standards at media buy creation. When absent, no structured performance standards apply.", + "items": { + "title": "Performance Standard", + "description": "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).", + "type": "object", + "properties": { + "metric": { + "$ref": "#/$defs/PerformanceStandardMetric" + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "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." + }, + "standard": { + "$ref": "#/$defs/ViewabilityStandard" + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor measuring this metric (e.g., { domain: 'doubleverify.com' }). The vendor's brand.json agents array (type: 'measurement') is the discovery point for their measurement agent. When specified on a confirmed package, creatives MUST include tracker_script or tracker_pixel assets from this vendor.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + } + }, + "required": [ + "metric", + "threshold", + "vendor" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "cancellation_policy": { + "title": "Cancellation Policy", + "description": "Cancellation terms for this product. Declares the minimum notice period required before cancellation takes effect and any penalties for insufficient notice. Relevant for guaranteed delivery products. Buyers accept these terms by creating a media buy against the product.", + "type": "object", + "properties": { + "notice_period": { + "title": "Duration", + "description": "Minimum notice period before cancellation takes effect (e.g., { interval: 30, unit: 'days' }). A guaranteed buy canceled without sufficient notice incurs the declared cancellation fee.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + }, + "cancellation_fee": { + "type": "object", + "description": "Fee applied when the notice period is not met.", + "properties": { + "type": { + "type": "string", + "enum": [ + "percent_remaining", + "full_commitment", + "fixed_fee", + "none" + ], + "description": "Fee calculation method. 'percent_remaining': percentage of remaining uncommitted spend. 'full_commitment': buyer owes the full committed budget regardless of delivery. 'fixed_fee': flat monetary amount. 'none': no financial fee (cancellation with notice is free)." + }, + "rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fee rate as a decimal proportion of remaining committed spend. Required when type is 'percent_remaining' (e.g., 0.5 means 50% of remaining spend)." + }, + "amount": { + "type": "number", + "minimum": 0, + "description": "Fixed fee amount in the buy's currency. Required when type is 'fixed_fee'." + } + }, + "required": [ + "type" + ], + "additionalProperties": true + } + }, + "required": [ + "notice_period", + "cancellation_fee" + ], + "additionalProperties": true + }, + "allowed_actions": { + "type": "array", + "description": "Actions buyers may perform on buys created against this product, scoped to statuses and modes. Advisory template \u2014 the authoritative per-buy capability is `available_actions[]` on the buy response, which resolves modes against current buy state, account tier, and negotiated terms. Buyers SHOULD use this for pre-flight product selection (\"which products let me self-serve cancel within 72hr?\") and read `available_actions[]` for runtime decisions. The array is uniquely keyed by `action` \u2014 sellers MUST NOT emit two entries with the same `action` value. Absence means the seller has not declared a structured action surface for this product \u2014 buyers fall back to `valid_actions[]` on buy responses for the flat string vocabulary.", + "items": { + "title": "Product Allowed Action", + "description": "An action a seller declares as allowed on buys created against this product, scoped to the buy statuses where the action is permitted and the modes available. Advisory template only \u2014 the authoritative per-buy resolution lives in `available_actions[]` on the buy response (which may diverge from the product template based on negotiated terms, account tier, or buy-level overrides). The containing `allowed_actions[]` array is uniquely keyed by `action`; sellers MUST NOT emit two entries with the same `action` value. JSON Schema `uniqueItems` only catches structurally identical objects, so validators MUST enforce action-uniqueness separately.", + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/MediaBuyValidAction" + }, + "modes": { + "type": "array", + "description": "Modes available for this action on this product. A product may declare multiple modes (for example `self_serve` within tolerances, escalating to `requires_approval` outside) \u2014 the buy-side `available_actions[].mode` resolves to the singular mode in effect at mutation time. SDKs that see multiple modes MUST NOT assume which one will fire; they must read the resolved `mode` on the buy.", + "items": { + "$ref": "#/$defs/MediaBuyActionMode" + }, + "minItems": 1, + "uniqueItems": true + }, + "allowed_statuses": { + "type": "array", + "description": "Media buy statuses in which this action is permitted. When absent, the action is permitted in all non-terminal statuses (`pending_creatives`, `pending_start`, `active`, `paused`).", + "items": { + "$ref": "#/$defs/MediaBuyStatus" + }, + "minItems": 1, + "uniqueItems": true + }, + "sla": { + "title": "SLA Window", + "description": "Optional SLA commitment for this action on this product. Absence means no commitment.", + "type": "object", + "properties": { + "response_max": { + "type": "string", + "description": "Maximum time from when the buyer issues the action to when the seller acknowledges receipt (mode-appropriate: synchronous response for self_serve, queue ack for requires_approval, proposal task creation for requires_proposal). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT5M", + "PT4H", + "P1D" + ] + }, + "completion_max": { + "type": "string", + "description": "Maximum time from buyer issuing the action to the seller completing it (mutation applied, proposal finalized, approval resolved). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT1H", + "PT24H", + "P2D" + ] + } + }, + "additionalProperties": false + }, + "terms_ref": { + "type": "string", + "description": "Optional pointer into buy-terms negotiation (forward-references the buy-terms namespace landing via separate RFC). When present, the named term governs cancellation policy, makegoods, or other commercial remedies tied to this action. Schema accepts any string for now and will tighten to a structured reference when the buy-terms RFC ships." + } + }, + "required": [ + "action", + "modes" + ], + "additionalProperties": false + }, + "minItems": 1, + "uniqueItems": true + }, + "reporting_capabilities": { + "title": "Reporting Capabilities", + "description": "Reporting capabilities available for a product", + "type": "object", + "properties": { + "available_reporting_frequencies": { + "type": "array", + "description": "Supported reporting frequency options", + "items": { + "$ref": "#/$defs/ReportingFrequency" + }, + "minItems": 1, + "uniqueItems": true + }, + "expected_delay_minutes": { + "type": "integer", + "description": "Expected delay in minutes before reporting data becomes available (e.g., 240 for 4-hour delay)", + "minimum": 0, + "examples": [ + 240, + 300, + 1440 + ] + }, + "timezone": { + "type": "string", + "description": "Timezone for reporting periods. Use 'UTC' or IANA timezone (e.g., 'America/New_York'). Critical for daily/monthly frequency alignment.", + "examples": [ + "UTC", + "America/New_York", + "Europe/London", + "America/Los_Angeles" + ] + }, + "supports_webhooks": { + "type": "boolean", + "description": "Whether this product supports webhook-based reporting notifications" + }, + "available_metrics": { + "type": "array", + "description": "Metrics available in reporting. Impressions and spend are always implicitly included. When a creative format declares reported_metrics, buyers receive the intersection of these product-level metrics and the format's reported_metrics.", + "items": { + "$ref": "#/$defs/AvailableMetric" + }, + "uniqueItems": true, + "examples": [ + [ + "impressions", + "spend", + "clicks", + "completed_views" + ], + [ + "impressions", + "spend", + "conversions" + ] + ] + }, + "vendor_metrics": { + "type": "array", + "description": "Vendor-defined metrics this product can report, beyond the closed `available_metrics` enum. Each entry is a pointer (`{ vendor, metric_id }`) into the vendor's metric catalog \u2014 the canonical definition (standard alignment, accreditations, methodology, unit, human-readable description) lives at the vendor's `get_adcp_capabilities.measurement.metrics[]`, queried once per vendor when needed. Use this for proprietary metrics like attention scores, emissions, panel-based demographics, or platform-native social metrics not yet in the standard enum. Sellers populate values in delivery via `delivery-metrics.json#/properties/vendor_metric_values`. The metric is identified by the tuple `(vendor, metric_id)`; identifiers are namespaced by the vendor, so the same `metric_id` may mean different things in different vendors' vocabularies. Semantic uniqueness key is `(vendor.domain, vendor.brand_id, metric_id)`; sellers MUST de-duplicate before emission and MUST NOT declare the same vendor metric twice. Buyers MAY treat duplicate `(vendor, metric_id)` rows as a seller-side conformance bug. (JSON Schema `uniqueItems` is not used here because BrandRef carries optional fields whose absence/presence would defeat deep-equal \u2014 uniqueness is on the semantic key, enforced at build/validation time on the seller side.) Promotion path: when the industry converges on a metric via a published standard, the spec adds it to the closed `available_metrics` enum and the vendor extensions become historical aliases.", + "items": { + "type": "object", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. The vendor's `brand.json` is the discovery anchor for the measurement agent (entry with `type: 'measurement'` in the `agents[]` array); the metric's standard alignment, accreditations, and methodology live at that agent's `get_adcp_capabilities.measurement.metrics[]` and are not duplicated inline here.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary (e.g., `attention_units`, `gco2e_per_impression`, `demographic_reach`).", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + } + }, + "required": [ + "vendor", + "metric_id" + ], + "additionalProperties": false + } + }, + "supports_creative_breakdown": { + "type": "boolean", + "description": "Whether this product supports creative-level metric breakdowns in delivery reporting (by_creative within by_package)" + }, + "supports_keyword_breakdown": { + "type": "boolean", + "description": "Whether this product supports keyword-level metric breakdowns in delivery reporting (by_keyword within by_package)" + }, + "supports_geo_breakdown": { + "title": "Geographic Breakdown Support", + "description": "Geographic breakdown support for this product. Declares which geo levels and systems are available for by_geo reporting within by_package.", + "type": "object", + "properties": { + "country": { + "type": "boolean", + "description": "Supports country-level geo breakdown (ISO 3166-1 alpha-2)" + }, + "region": { + "type": "boolean", + "description": "Supports region/state-level geo breakdown (ISO 3166-2)" + }, + "metro": { + "type": "object", + "description": "Metro area breakdown support. Keys are metro-system enum values; true means supported.", + "propertyNames": { + "$ref": "#/$defs/MetroAreaSystem" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "postal_area": { + "type": "object", + "description": "Postal area breakdown support. Keys are postal-system enum values; true means supported.", + "propertyNames": { + "$ref": "#/$defs/PostalCodeSystem" + }, + "additionalProperties": { + "type": "boolean" + } + } + }, + "additionalProperties": false + }, + "supports_device_type_breakdown": { + "type": "boolean", + "description": "Whether this product supports device type breakdowns in delivery reporting (by_device_type within by_package)" + }, + "supports_device_platform_breakdown": { + "type": "boolean", + "description": "Whether this product supports device platform breakdowns in delivery reporting (by_device_platform within by_package)" + }, + "supports_audience_breakdown": { + "type": "boolean", + "description": "Whether this product supports audience segment breakdowns in delivery reporting (by_audience within by_package)" + }, + "supports_placement_breakdown": { + "type": "boolean", + "description": "Whether this product supports placement breakdowns in delivery reporting (by_placement within by_package)" + }, + "date_range_support": { + "type": "string", + "enum": [ + "date_range", + "lifetime_only" + ], + "description": "Whether delivery data can be filtered to arbitrary date ranges. 'date_range' means the platform supports start_date/end_date parameters. 'lifetime_only' means the platform returns campaign lifetime totals and date range parameters are not accepted.", + "default": "date_range" + }, + "windowed_pull_granularities": { + "type": "array", + "description": "Granularities at which this product honors per-window pulls on get_media_buy_delivery (via request `time_granularity` + `include_window_breakdown: true`). Closes the GET-side half of the snapshot/log two-paths-parity contract for data-bearing events: a buyer who missed a webhook fire at any granularity listed here can reconstruct an identical payload by polling. Capability-scoped MUST \u2014 sellers MUST honor pulls at any granularity declared here, and MUST return UNSUPPORTED_GRANULARITY for pulls outside the set. Sellers MAY emit higher-frequency webhooks than they expose for pull (common where the webhook is a Kafka tap and historical reads go through a warehouse with coarser granularity); buyers see the gap up front via this capability and treat the webhook as primary for those frequencies. Absent or empty means the product only supports cumulative date-range pulls and full per-window recovery via GET is unavailable \u2014 see snapshot-and-log Rule 4.", + "items": { + "$ref": "#/$defs/ReportingFrequency" + }, + "uniqueItems": true, + "examples": [ + [ + "daily" + ], + [ + "hourly", + "daily" + ], + [ + "hourly", + "daily", + "monthly" + ] + ] + }, + "measurement_windows": { + "type": "array", + "description": "Measurement maturation stages available for this product. Used by any channel where billing-grade data is produced in phases rather than arriving final on day one. Examples: broadcast/linear TV (Live \u2192 C3 \u2192 C7 DVR accumulation), DOOH (tentative plays \u2192 post-IVT/fraud-check final), digital with IVT filtering (raw \u2192 GIVT filtered \u2192 SIVT filtered), podcast (7-day downloads \u2192 30-day downloads). Each window defines an accumulation period and expected data availability. When present, delivery reports reference a specific window_id. Sellers whose data is final on first delivery typically omit this.", + "items": { + "title": "Measurement Window", + "description": "A measurement maturation stage for any channel where billing-grade data is produced in phases rather than arriving final on day one. Each window represents an accumulation or processing stage with its own expected availability. Examples: broadcast/linear TV (live \u2192 C3 \u2192 C7 DVR accumulation), DOOH (tentative plays \u2192 post-IVT/fraud-check final), digital (raw impressions \u2192 GIVT filtered \u2192 SIVT filtered), podcast (7-day downloads \u2192 30-day downloads), audio/radio (tentative \u2192 diary/panel-certified). Sellers whose data is final on first delivery omit this.", + "type": "object", + "properties": { + "window_id": { + "type": "string", + "maxLength": 50, + "description": "Identifier for this maturation stage. Standard broadcast values: 'live' (real-time viewers only), 'c3' (live + 3 days time-shifted), 'c7' (live + 7 days time-shifted). Standard values for other channels include 'tentative' (provisional data available quickly), 'final' (post-processing certified data), 'post_ivt' (digital after invalid-traffic filtering), 'post_sivt' (digital after sophisticated-IVT filtering), 'downloads_7d' / 'downloads_30d' (podcast download maturation). Sellers may define custom IDs.", + "examples": [ + "live", + "c3", + "c7", + "tentative", + "final", + "post_ivt", + "downloads_30d" + ] + }, + "description": { + "type": "string", + "maxLength": 500, + "description": "Human-readable description of what this window measures", + "examples": [ + "Live broadcast impressions only", + "Live plus 7 days of time-shifted viewing", + "Tentative plays before IVT and fraud-check processing", + "Final plays after IVT and fraud-check processing", + "Impressions after sophisticated invalid-traffic filtering" + ] + }, + "duration_days": { + "type": "integer", + "description": "Number of days of accumulation included in this window before processing begins. For broadcast, this is DVR accumulation (0 = live only, 3 = live + 3 days DVR, 7 = live + 7 days DVR). For channels without an accumulation period (DOOH tentative\u2192final, digital IVT filtering), this is 0 \u2014 maturation is entirely vendor processing time captured in expected_availability_days.", + "minimum": 0 + }, + "expected_availability_days": { + "type": "integer", + "description": "Expected number of days after delivery before this window's data is available from the measurement vendor. Captures accumulation time plus vendor processing time. Examples: broadcast C7 from VideoAmp ~22 days (7-day accumulation + ~15-day processing); DOOH tentative plays same-day; DOOH final (post-IVT/fraud-check) ~1 day; digital post-SIVT ~2\u20133 days.", + "minimum": 0 + }, + "is_guarantee_basis": { + "type": "boolean", + "description": "Whether this window is the basis for delivery guarantees, reconciliation, and invoicing. A product typically has one guarantee basis window (e.g., C7 for most US broadcast, post-IVT final for DOOH). Buyers reconcile against the guarantee basis window's final numbers." + } + }, + "required": [ + "window_id", + "duration_days" + ], + "additionalProperties": true + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "available_reporting_frequencies", + "expected_delay_minutes", + "timezone", + "supports_webhooks", + "available_metrics", + "date_range_support" + ], + "additionalProperties": true + }, + "creative_policy": { + "title": "Creative Policy", + "description": "Creative requirements and restrictions for a product", + "type": "object", + "properties": { + "co_branding": { + "$ref": "#/$defs/CoBrandingRequirement" + }, + "landing_page": { + "$ref": "#/$defs/LandingPageRequirement" + }, + "templates_available": { + "type": "boolean", + "description": "Whether creative templates are provided" + }, + "provenance_required": { + "type": "boolean", + "description": "Whether creatives must include provenance metadata. When true, the seller requires buyers to attach provenance declarations to creative submissions. The seller may independently verify claims via get_creative_features." + }, + "provenance_requirements": { + "type": "object", + "description": "Structured provenance requirements for creatives. Refines `provenance_required`: when `provenance_required` is true, the fields in this object specify which provenance features the seller requires. When `provenance_required` is false or absent, this object SHOULD be absent; if present, receivers MUST ignore it. Existing seller agents that do not read this object are unaffected; the wire shape does not change for them. Sellers that publish a requirement here MUST enforce it on creative submission: a `sync_creatives` request that omits a required field is rejected with the corresponding `PROVENANCE_*` error code (see error-code.json), and a creative whose provenance claim is contradicted by an independent verification (`get_creative_features` against a governance agent the seller operates or has allowlisted via `accepted_verifiers`) is rejected with `PROVENANCE_CLAIM_CONTRADICTED`. This is the structural-rejection surface; the truth-of-claim surface lives in `get_creative_features`. Field-level requirements are seller-enforced \u2014 JSON Schema validation does not check them.", + "properties": { + "require_digital_source_type": { + "type": "boolean", + "description": "When true, the seller requires creatives to include a `digital_source_type` field in their provenance, set to a valid value from the `digital-source-type` enum (not null or absent). Submissions that omit this field are rejected with `PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING`. Supports EU AI Act Art. 50 and CA SB 942 compliance workflows where AI disclosure metadata must be present at the protocol level." + }, + "require_disclosure_metadata": { + "type": "boolean", + "description": "When true, the seller requires creatives to include a `disclosure` object in their provenance with `disclosure.required` set to a boolean value (true or false). When `disclosure.required` is true, at least one entry in `disclosure.jurisdictions` is expected. Submissions that omit `disclosure.required` are rejected with `PROVENANCE_DISCLOSURE_MISSING`." + }, + "require_embedded_provenance": { + "type": "boolean", + "description": "When true, the seller requires creatives to include at least one `embedded_provenance` entry. For pipelines where sidecar metadata is stripped by intermediaries, this ensures provenance data persists through delivery. Submissions that omit `embedded_provenance` are rejected with `PROVENANCE_EMBEDDED_MISSING`." + } + }, + "additionalProperties": true + }, + "accepted_verifiers": { + "type": "array", + "description": "Governance agents the seller operates, has allowlisted, or otherwise trusts to verify provenance claims via `get_creative_features`. Buyers attaching a `verify_agent` pointer on `embedded_provenance[]` or `watermarks[]` MUST select an `agent_url` that appears in this list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments) - the buyer is *representing* that they used a verifier the seller will recognize, not asserting unilateral routing. Sellers MUST reject `sync_creatives` submissions whose `verify_agent.agent_url` does not match any entry here with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. The seller is the verifier-of-record: it is the seller, not the buyer, that decides which agent it will call. Publishing the list lets buyers pre-flight their creative shape against `get_products` and lets multiple buyers converge on the same verifier without coordinating with each other.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "URL of the governance agent. MUST use the `https://` scheme. The seller calls this URL via `get_creative_features` to verify a buyer's claim; the seller has already vetted the endpoint and accepts responsibility for outbound calls to it." + }, + "feature_id": { + "type": "string", + "description": "Optional canonical `feature_id` the seller will request against this agent (e.g., `encypher.markers_present_v2`). When present, the buyer's `verify_agent.feature_id` SHOULD either match this value or be omitted. When absent, the seller selects a feature from the agent's `governance.creative_features` catalog at evaluation time. Resolves the selector ambiguity that would otherwise let two compliant receivers reach different verdicts." + }, + "providers": { + "type": "array", + "description": "Optional `provider` labels this agent verifies (e.g., `['Encypher', 'Digimarc']`). When present, sellers SHOULD only invoke this agent for `embedded_provenance[]` / `watermarks[]` entries whose `provider` field matches one of these labels \u2014 letting buyers pre-flight whether their attached evidence is verifiable against the seller's allowlist. When absent, the agent is treated as provider-agnostic.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + } + } + }, + "required": [ + "co_branding", + "landing_page", + "templates_available" + ], + "additionalProperties": true + }, + "is_custom": { + "type": "boolean", + "description": "Whether this is a custom product" + }, + "property_targeting_allowed": { + "type": "boolean", + "default": false, + "description": "Whether buyers can filter this product to a subset of its publisher_properties. When false (default), the product is 'all or nothing' - buyers must accept all properties or the product is excluded from property_list filtering results." + }, + "data_provider_signals": { + "type": "array", + "description": "Deprecated. Legacy/non-selectable metadata for data-provider catalog signals already bundled into or associated with this product. This field does not provide buyer-selectable options, prices, or seller activation handles. Use included_signals for non-selectable product signal metadata, or signal_targeting_options for selectable package-level signal groups.", + "deprecated": true, + "items": { + "$ref": "#/$defs/DataProviderSignalSelector" + } + }, + "included_signals": { + "type": "array", + "description": "Non-selectable signal metadata for signals already included in, bundled with, or planned into this product. These signals describe what the product is; buyers do not select them in packages[].targeting_overlay.signal_targeting_groups and this field does not imply package-level signal targeting. Use signal_ref scope 'data_provider' or 'signal_source' to reference externally defined signals without redefining their name or value_type. Use signal_ref scope 'product' with name and value_type when the included signal is defined only by this product.", + "items": { + "title": "Signal Listing", + "description": "Shared signal identity and definition metadata used when a signal is listed outside its authoritative catalog. New listings carry signal_ref; legacy listings may carry deprecated signal_id during the SignalRef migration window. Product-local signals use the listing as the definition boundary and MUST include name and value_type. Data-provider and signal-source refs MAY omit definition metadata when the buyer can resolve it from the referenced catalog or source; any supplied name, description, value_type, categories, range, methodology_url, or last_updated is product/account/source context and does not replace the authoritative definition.", + "type": "object", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined by a data provider's published adagents.json signal catalog; use scope 'signal_source' with signal_source_url for a source-native signal.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "name": { + "type": "string", + "description": "Human-readable signal name. Required when signal_ref.scope is 'product'. For data_provider and signal_source refs, this is optional contextual display text; the referenced catalog or source remains authoritative." + }, + "description": { + "type": "string", + "description": "Detailed signal description. For data_provider and signal_source refs, this is optional contextual display text and MUST NOT replace the referenced definition." + }, + "methodology_url": { + "type": "string", + "format": "uri", + "description": "Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition." + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "When this listing record was last updated. This indicates freshness of the listing record, not an attestation that the underlying data or model was refreshed at that time." + }, + "value_type": { + "$ref": "#/$defs/SignalValueType" + }, + "categories": { + "type": "array", + "description": "Valid values for categorical signals. Present when value_type is 'categorical'.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "range": { + "type": "object", + "description": "Valid range for numeric signals. Present when value_type is 'numeric'.", + "properties": { + "min": { + "type": "number", + "description": "Minimum value, inclusive." + }, + "max": { + "type": "number", + "description": "Maximum value, inclusive." + } + }, + "required": [ + "min", + "max" + ], + "additionalProperties": false + } + }, + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "allOf": [ + { + "description": "Product-local signal listings define the signal inline, so they need a display name and value type.", + "if": { + "properties": { + "signal_ref": { + "type": "object", + "properties": { + "scope": { + "const": "product" + } + }, + "required": [ + "scope" + ] + } + }, + "required": [ + "signal_ref" + ] + }, + "then": { + "required": [ + "name", + "value_type" + ] + } + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "signal_targeting_options": { + "type": "array", + "description": "Inline seller-offered signals that may be applied to packages for this product at create_media_buy time. Each entry references a named signal definition with signal_ref scope 'product' for a product-local signal option, scope 'data_provider' for an external published adagents.json signal catalog the seller is authorized to apply, or scope 'signal_source' for a source-native signal. Product-local options define name and value_type inline; data-provider and signal-source options may omit those fields when the referenced catalog or source is authoritative. Use this field when the selectable menu is product-specific, has product-specific pricing or activation handles, is the relevant subset for a brief/refine result, or should be rendered without an additional get_signals call. Wholesale products may omit this field and rely on get_signals for the selectable signal feed. Buyers select eligible signals through packages[].targeting_overlay.signal_targeting_groups when signal_targeting_rules allow; fixed/default entries are applied by the seller and echoed on the package state. Sellers MUST set signal_targeting_allowed to true whenever this field is present. Bundled, non-selectable signal metadata belongs in included_signals; legacy data_provider_signals may appear only for backwards compatibility.", + "items": { + "title": "Product Signal Targeting Option", + "description": "A signal the seller makes available inline for package-level signal composition on this product. Product.signal_targeting_options is used when the product needs product-scoped pricing, activation handles, defaults, grouping hints, a brief/refine-selected subset, or a curated inline menu. Wholesale products can instead omit inline options when the selectable menu is the broader get_signals feed. Product-local signals define their name and value_type inline through the shared signal-listing fields; data-provider and signal-source refs may omit those definition fields when the referenced catalog or source is authoritative.", + "type": "object", + "allOf": [ + { + "title": "Signal Listing", + "description": "Shared signal identity and definition metadata used when a signal is listed outside its authoritative catalog. New listings carry signal_ref; legacy listings may carry deprecated signal_id during the SignalRef migration window. Product-local signals use the listing as the definition boundary and MUST include name and value_type. Data-provider and signal-source refs MAY omit definition metadata when the buyer can resolve it from the referenced catalog or source; any supplied name, description, value_type, categories, range, methodology_url, or last_updated is product/account/source context and does not replace the authoritative definition.", + "type": "object", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined by a data provider's published adagents.json signal catalog; use scope 'signal_source' with signal_source_url for a source-native signal.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "name": { + "type": "string", + "description": "Human-readable signal name. Required when signal_ref.scope is 'product'. For data_provider and signal_source refs, this is optional contextual display text; the referenced catalog or source remains authoritative." + }, + "description": { + "type": "string", + "description": "Detailed signal description. For data_provider and signal_source refs, this is optional contextual display text and MUST NOT replace the referenced definition." + }, + "methodology_url": { + "type": "string", + "format": "uri", + "description": "Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition." + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "When this listing record was last updated. This indicates freshness of the listing record, not an attestation that the underlying data or model was refreshed at that time." + }, + "value_type": { + "$ref": "#/$defs/SignalValueType" + }, + "categories": { + "type": "array", + "description": "Valid values for categorical signals. Present when value_type is 'categorical'.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "range": { + "type": "object", + "description": "Valid range for numeric signals. Present when value_type is 'numeric'.", + "properties": { + "min": { + "type": "number", + "description": "Minimum value, inclusive." + }, + "max": { + "type": "number", + "description": "Maximum value, inclusive." + } + }, + "required": [ + "min", + "max" + ], + "additionalProperties": false + } + }, + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "allOf": [ + { + "description": "Product-local signal listings define the signal inline, so they need a display name and value type.", + "if": { + "properties": { + "signal_ref": { + "type": "object", + "properties": { + "scope": { + "const": "product" + } + }, + "required": [ + "scope" + ] + } + }, + "required": [ + "signal_ref" + ] + }, + "then": { + "required": [ + "name", + "value_type" + ] + } + } + ], + "additionalProperties": true + }, + { + "description": "Signals that require activation need an operational handle the buyer can pass to activate_signal before package selection.", + "if": { + "properties": { + "activation_status": { + "const": "requires_activation" + } + }, + "required": [ + "activation_status" + ] + }, + "then": { + "required": [ + "signal_agent_segment_id" + ] + } + } + ], + "required": [ + "signal_ref" + ], + "properties": { + "signal_agent_segment_id": { + "type": "string", + "description": "Optional opaque seller execution handle for this signal. Omit when signal_ref is sufficient for the seller to resolve the signal. Include only when the seller exposes a distinct runtime or activation handle that buyers must echo in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].signal_agent_segment_id.", + "x-entity": "signal_activation_id" + }, + "activation_status": { + "type": "string", + "description": "Whether this signal option is ready to select on create_media_buy for the requesting account. 'ready' means the buyer can select it directly. 'requires_activation' means the buyer must activate the signal first or include an activation_key the seller accepts.", + "enum": [ + "ready", + "requires_activation" + ], + "default": "ready" + }, + "allowed_targeting_modes": { + "type": "array", + "description": "How this signal may be used when composing package-level signal targeting groups. 'include' means the signal may appear in an 'any' child group. 'exclude' means the signal may appear in a 'none' child group. Omit when the signal is include-only. This field declares the allowed buy-time group operator; binary package signal entries still use value=true in both include and exclude groups.", + "items": { + "type": "string", + "enum": [ + "include", + "exclude" + ] + }, + "uniqueItems": true, + "minItems": 1, + "default": [ + "include" + ] + }, + "default_selected": { + "type": "boolean", + "description": "Whether the seller recommends or preselects this signal when composing this product. Buyers may remove it unless signal_targeting_rules.selection_mode is 'fixed'. When selection_mode is 'fixed', sellers apply default_selected signals even if the buyer omits signal_targeting_groups and MUST echo the applied entries on the resulting package state.", + "default": false + }, + "selection_group": { + "type": "string", + "description": "Optional product-defined composability bucket for signal options, such as alternative audience tiers, a key-value targeting plane, or an audience-segment targeting plane. Signals in the same selection_group are expected to be OR-combinable inside one child group for a given targeting mode, subject to signal_targeting_rules. Use different selection_group values when the product requires separate ANDed clauses, such as signal sets backed by different platform targeting primitives that cannot be collapsed into one child group. selection_group is a product-option grouping key, not a reference to one child object in packages[].targeting_overlay.signal_targeting_groups.groups[]. Sellers can use signal_targeting_rules.max_selected_per_group and signal_targeting_rules.selection_group_rules with selection_group to guide and validate storefront composition." + }, + "pricing_options": { + "type": "array", + "description": "Signal pricing options available when this signal is selected on this product. Product-scoped pricing is authoritative for this product; if get_signals exposes a different default rate card, use this product-scoped price when composing the buy. Buyers pass the selected pricing_option_id in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].pricing_option_id. Omit when the signal is bundled into the product price or has no incremental cost.", + "items": { + "title": "Vendor Pricing Option", + "description": "A pricing option offered by a vendor agent (signals, creative, governance). Combines pricing_option_id with the pricing model fields. Pass pricing_option_id in report_usage for billing verification. All vendor discovery responses return pricing_options as an array \u2014 vendors may offer multiple options (volume tiers, context-specific rates, different models per product line).", + "allOf": [ + { + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Opaque identifier for this pricing option, unique within the vendor agent. Pass this in report_usage to identify which pricing option was applied.", + "x-entity": "vendor_pricing_option" + } + }, + "required": [ + "pricing_option_id" + ] + }, + { + "title": "Vendor Pricing", + "description": "Pricing model for a vendor service. Discriminated by model: 'cpm' (fixed CPM), 'percent_of_media' (percentage of spend with optional CPM cap), 'flat_fee' (fixed charge per reporting period), 'per_unit' (fixed price per unit of work), or 'custom' (escape hatch for models not covered by the enumerated forms \u2014 requires a description and structured metadata).", + "type": "object", + "discriminator": { + "propertyName": "model" + }, + "oneOf": [ + { + "title": "CpmPricing", + "description": "Fixed cost per thousand impressions", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "cpm" + }, + "cpm": { + "type": "number", + "description": "Cost per thousand impressions", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "cpm", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PercentOfMediaPricing", + "description": "Percentage of media spend charged for this signal. When max_cpm is set, the effective rate is capped at that CPM \u2014 useful for platforms like The Trade Desk that use percent-of-media pricing with a CPM ceiling.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "percent_of_media" + }, + "percent": { + "type": "number", + "description": "Percentage of media spend, e.g. 15 = 15%", + "minimum": 0, + "maximum": 100 + }, + "max_cpm": { + "type": "number", + "description": "Optional CPM cap. When set, the effective charge is min(percent \u00d7 media_spend_per_mille, max_cpm).", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for the resulting charge", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "percent", + "currency" + ], + "additionalProperties": true + }, + { + "title": "FlatFeePricing", + "description": "Fixed charge per billing period, regardless of impressions or spend. Used for licensed data bundles and audience subscriptions.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "flat_fee" + }, + "amount": { + "type": "number", + "description": "Fixed charge for the billing period", + "minimum": 0 + }, + "period": { + "type": "string", + "enum": [ + "monthly", + "quarterly", + "annual", + "campaign" + ], + "description": "Billing period for the flat fee." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "amount", + "period", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PerUnitPricing", + "description": "Fixed price per unit of work. Used for creative transformation (per format), AI generation (per image, per token), and rendering (per variant). The unit field describes what is counted; unit_price is the cost per one unit.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "per_unit" + }, + "unit": { + "type": "string", + "description": "What is counted \u2014 e.g. 'format', 'image', 'token', 'variant', 'render', 'evaluation'." + }, + "unit_price": { + "type": "number", + "description": "Cost per one unit", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "unit", + "unit_price", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CustomPricing", + "description": "Escape hatch for pricing constructs that do not fit cpm, percent_of_media, flat_fee, or per_unit. Use when a vendor prices via performance kickers, tiered volume, hybrid formulas, outcome-sharing, or any other model the standard forms cannot express. Requires a human-readable description and a structured metadata object that captures the parameters a buyer needs to reason about the charge. Buyers SHOULD route custom pricing through operator review before commitment \u2014 automatic selection is not recommended.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "custom" + }, + "description": { + "type": "string", + "description": "Human-readable description of the custom pricing model. Buyers display this to the operator when requesting approval.", + "minLength": 1 + }, + "metadata": { + "type": "object", + "description": "Structured parameters for the custom model. Keys follow lowercase_snake_case. Values may be primitives, arrays, or nested objects. Must be sufficient for a human to understand the pricing basis and for a downstream system to reconstruct the charge. Vendors SHOULD include a `summary_for_operator` string (one or two sentences, suitable for display in a buyer's operator-review UI) so reviewers across vendors see a consistent prompt. Required operator-review fields (approver role, dollar threshold for automatic approval, escalation contact) MAY be surfaced via additional keys the buyer's review surface recognizes.", + "additionalProperties": true, + "minProperties": 1, + "properties": { + "summary_for_operator": { + "type": "string", + "description": "One or two sentences describing the pricing construct in plain language, displayed to the buyer's operator when requesting approval. Should not repeat the top-level `description` verbatim \u2014 summarize the charge mechanic instead (e.g., 'Base $12 CPM plus $0.50 per qualifying post-view conversion, capped at $45 CPM').", + "minLength": 1 + } + } + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. Present when the pricing resolves to a monetary charge in a specific currency.", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "description", + "metadata" + ], + "additionalProperties": true + } + ] + } + ] + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "minItems": 1 + }, + "signal_targeting_rules": { + "title": "Signal Targeting Rules", + "description": "Composition rules for selecting signals on this product. The selectable signal menu may come from inline signal_targeting_options or from get_signals when a wholesale product omits inline options. This is product-scoped because products may be backed by different ad servers with different Boolean targeting support and group limits.", + "type": "object", + "properties": { + "resolution_model": { + "type": "string", + "description": "How selected signal_targeting_options are resolved against the product's inventory. 'direct_targeting' means selected signals are applied as targeting predicates to the package inventory. 'seller_planned' means selected signals are planning inputs that the seller resolves against product-specific inventory, timing, availability, reach, or pacing constraints; buyers SHOULD NOT attempt to decompose the signal selection into lower-level inventory or schedule decisions. Use 'seller_planned' for products such as linear broadcast schedules where the audience definition may be portable but the audience-to-avails plan is seller-resolved.", + "enum": [ + "direct_targeting", + "seller_planned" + ], + "default": "direct_targeting" + }, + "selection_mode": { + "type": "string", + "description": "Default selection behavior for selectable signals on this product. 'optional' means the buyer may select zero or more signals. 'required' means the buyer must select at least min_selected_signals, or 1 when min_selected_signals is omitted. 'fixed' means the seller applies the default_selected signals and the buyer cannot add or remove them; buyers SHOULD render those entries as read-only and sellers MUST echo them in package targeting_overlay.signal_targeting_groups. Use selection_group_rules for product-scoped products that need different behavior for different groups, such as fixed suppressions plus a required include tier.", + "enum": [ + "optional", + "required", + "fixed" + ], + "default": "optional" + }, + "min_selected_signals": { + "type": "integer", + "description": "Minimum number of signals the buyer must select when selection_mode is 'required'. If selection_mode is 'required' and this field is omitted, sellers MUST treat the minimum as 1. Defaults to 0 for optional selection.", + "minimum": 0 + }, + "max_selected_signals": { + "type": "integer", + "description": "Maximum number of signals the buyer may select for a package. Omit when there is no declared limit beyond the available options.", + "minimum": 1 + }, + "max_selected_per_group": { + "type": "integer", + "description": "Maximum number of signal_targeting_options the buyer may select from the same ProductSignalTargetingOption.selection_group. Use 1 for mutually exclusive alternatives within each option group. This limit applies to product option grouping, not to the number of child groups in packages[].targeting_overlay.signal_targeting_groups.", + "minimum": 1 + }, + "max_signal_targeting_groups": { + "type": "integer", + "description": "Maximum number of child groups allowed in packages[].targeting_overlay.signal_targeting_groups.groups. Omit when the seller has no declared limit beyond product terms.", + "minimum": 1 + }, + "max_signals_per_targeting_group": { + "type": "integer", + "description": "Maximum number of signals allowed in each packages[].targeting_overlay.signal_targeting_groups.groups[].signals array. Omit when the seller has no declared limit beyond product terms.", + "minimum": 1 + }, + "selection_group_rules": { + "type": "array", + "description": "Optional product-scoped overrides for specific ProductSignalTargetingOption.selection_group values. Use this when one product has mixed behavior, such as fixed seller-applied suppressions, a required pick-one include tier, optional buyer-selected exclusions, or heterogeneous targeting planes that must be represented as separate ANDed clauses. Rules apply only to options whose selection_group matches. When selection_group_rules are present, each packages[].targeting_overlay.signal_targeting_groups child group MUST contain signals from exactly one selection_group and one targeting_mode, and buyers MUST send at most one child group for each (selection_group, targeting_mode) pair. Sellers MUST reject duplicate, mixed, or collapsed groups that combine distinct selection_group_rules into the same child group.", + "items": { + "type": "object", + "properties": { + "selection_group": { + "type": "string", + "description": "ProductSignalTargetingOption.selection_group value this rule applies to." + }, + "targeting_mode": { + "type": "string", + "description": "How options in this selection_group are intended to be used in signal_targeting_groups. 'include' maps to child groups with operator 'any'. 'exclude' maps to child groups with operator 'none'. Omit when options in the group may be used according to each option's allowed_targeting_modes.", + "enum": [ + "include", + "exclude" + ] + }, + "selection_mode": { + "type": "string", + "description": "Selection behavior for this selection_group. 'required' means at least min_selected_signals, or 1 when omitted. 'fixed' means default_selected options in this group are seller-applied and read-only.", + "enum": [ + "optional", + "required", + "fixed" + ] + }, + "min_selected_signals": { + "type": "integer", + "description": "Minimum selected options from this selection_group. If selection_mode is 'required' and omitted, sellers MUST treat the minimum as 1.", + "minimum": 0 + }, + "max_selected_signals": { + "type": "integer", + "description": "Maximum selected options from this selection_group.", + "minimum": 1 + } + }, + "required": [ + "selection_group" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "signal_targeting_allowed": { + "type": "boolean", + "default": false, + "description": "Whether this product has a package-level signal_targeting_groups surface. When false (default), signals are bundled into the product terms and cannot be selected or explicitly echoed as package signal groups. When true, eligible signals from inline signal_targeting_options or from get_signals may be buyer-selected or seller-applied according to signal_targeting_rules and are represented through packages[].targeting_overlay.signal_targeting_groups. Editability is controlled by signal_targeting_rules; fixed/default-only products still set this to true when applied signal groups are echoed." + }, + "catalog_types": { + "type": "array", + "description": "Catalog types this product supports for catalog-driven campaigns. A sponsored product listing declares [\"product\"], a job board declares [\"job\", \"offering\"]. Buyers match synced catalogs to products via this field.", + "items": { + "$ref": "#/$defs/CatalogType" + }, + "uniqueItems": true, + "minItems": 1 + }, + "metric_optimization": { + "type": "object", + "description": "Metric optimization capabilities for this product. Presence indicates the product supports optimization_goals with kind: 'metric'. No event source or conversion tracking setup required \u2014 the seller tracks these metrics natively.", + "properties": { + "supported_metrics": { + "type": "array", + "description": "Metric kinds this product can optimize for. Buyers should only request metric goals for kinds listed here. **DEPRECATED values** (slated for removal at next major): `attention_seconds` and `attention_score` \u2014 declare vendor-attested attention/quality metrics via `vendor_metric_optimization.supported_metrics[]` with an explicit vendor binding instead. Sellers MAY reject the deprecated values with `TERMS_REJECTED` and a suggestion to use the `vendor_metric` kind.", + "items": { + "type": "string", + "enum": [ + "clicks", + "views", + "completed_views", + "viewed_seconds", + "attention_seconds", + "attention_score", + "engagements", + "follows", + "saves", + "profile_visits", + "reach" + ] + }, + "minItems": 1 + }, + "supported_reach_units": { + "type": "array", + "description": "Reach units this product can optimize for. Required when supported_metrics includes 'reach'. Buyers must set reach_unit to a value in this list on reach optimization goals \u2014 sellers reject unsupported values.", + "items": { + "$ref": "#/$defs/ReachUnit" + }, + "minItems": 1 + }, + "supported_view_durations": { + "type": "array", + "description": "Video view duration thresholds (in seconds) this product supports for completed_views goals. Only relevant when supported_metrics includes 'completed_views'. When absent, the seller uses their platform default. Buyers must set view_duration_seconds to a value in this list \u2014 sellers reject unsupported values.", + "items": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "supported_targets": { + "type": "array", + "description": "Target kinds available for metric goals on this product. Values match target.kind on the optimization goal. Only these target kinds are accepted \u2014 goals with unlisted target kinds will be rejected. When omitted, buyers can set target-less metric goals (maximize volume within budget) but cannot set specific targets.", + "items": { + "type": "string", + "enum": [ + "cost_per", + "threshold_rate" + ] + } + } + }, + "required": [ + "supported_metrics" + ], + "additionalProperties": true + }, + "vendor_metric_optimization": { + "title": "Vendor Metric Optimization", + "description": "Vendor-attested metric optimization capabilities for this product. Presence indicates the product supports `optimization_goals` with `kind: 'vendor_metric'` \u2014 the seller's bidding stack can steer delivery toward a specific vendor's measurement (e.g., DV/IAS/Adelaide attention, Scope3 emissions, Kantar brand lift, retail-media partner metrics). Distinct from `metric_optimization` (seller-native metrics with no vendor binding) and from `reporting_capabilities.vendor_metrics` (which declares what the product can *report* rather than what it can *optimize against*). A product may report a vendor metric without being able to optimize for it. Buyers MUST verify the goal's `(vendor, metric_id)` is in `supported_metrics` AND that the package's `committed_metrics[]` includes a matching `{ scope: 'vendor', vendor, metric_id }` entry \u2014 optimization without committed reporting is unverifiable and is rejected at the wire level.", + "type": "object", + "properties": { + "supported_metrics": { + "type": "array", + "description": "Vendor-defined metrics this product can steer delivery toward. Each entry pairs a vendor identity (BrandRef anchored on the vendor's `brand.json` `agents[type='measurement']`) with a `metric_id` from that vendor's published `measurement.metrics[]` catalog, plus the target kinds the seller supports for the pair. Semantic uniqueness key is `(vendor.domain, vendor.brand_id, metric_id)`; sellers MUST de-duplicate before publication. JSON Schema `uniqueItems` blocks exact-object duplicates; semantic deduplication on the BrandRef-domain key is a seller obligation.", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. The vendor's `brand.json` is the discovery anchor for the measurement agent (entry with `type: 'measurement'` in the `agents[]` array); the metric's definition, methodology, and unit live at that agent's `get_adcp_capabilities.measurement.metrics[]` and are not duplicated inline here. Same shape as the `vendor` field on `reporting_capabilities.vendor_metrics` for symmetry across optimization and reporting capability declarations.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary (e.g., `attention_score`, `attention_seconds`, `gco2e_per_impression`, `awareness_lift`). MUST be present in the vendor's published `measurement.metrics[]` catalog.", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + }, + "supported_targets": { + "type": "array", + "description": "Target kinds available for `vendor_metric` goals against this `(vendor, metric_id)` pair. Values match `target.kind` on the optimization goal. `cost_per` \u2014 target cost per metric unit (e.g., $0.05 per attention-second). `threshold_rate` \u2014 minimum per-impression value (e.g., attention_score \u2265 70). Only these target kinds are accepted \u2014 goals with unlisted target kinds will be rejected. A goal without a target implicitly maximizes the metric within budget \u2014 no declaration needed for that mode. When omitted, buyers can still set target-less vendor_metric goals.", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "cost_per", + "threshold_rate" + ] + } + } + }, + "required": [ + "vendor", + "metric_id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "supported_metrics" + ], + "additionalProperties": true + }, + "max_optimization_goals": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of optimization_goals this product accepts on a package. When absent, no limit is declared. Most social platforms accept only 1 goal \u2014 buyers sending arrays longer than this value should expect the seller to use only the highest-priority (lowest priority number) goal." + }, + "measurement_readiness": { + "title": "Measurement Readiness", + "description": "Assessment of whether the buyer's event source setup is sufficient for this product to optimize effectively. Only present when the seller can evaluate the buyer's account context. Buyers should check this before creating media buys with event-based optimization goals.", + "type": "object", + "properties": { + "status": { + "$ref": "#/$defs/AssessmentStatus" + }, + "required_event_types": { + "type": "array", + "description": "Event types this product needs for effective optimization. Buyers should ensure their event sources cover these types.", + "items": { + "$ref": "#/$defs/EventType" + }, + "minItems": 1 + }, + "missing_event_types": { + "type": "array", + "description": "Event types this product requires that the buyer has not configured. Empty or absent when all required types are covered.", + "items": { + "$ref": "#/$defs/EventType" + } + }, + "issues": { + "type": "array", + "description": "Actionable issues preventing full measurement readiness. Sellers should limit to the top 3-5 most actionable items. Buyer agents should sort by severity rather than relying on array position.", + "items": { + "title": "Diagnostic Issue", + "description": "An actionable issue detected during a health or readiness assessment. Used by event source health and measurement readiness to surface problems and recommendations.", + "type": "object", + "properties": { + "severity": { + "type": "string", + "enum": [ + "error", + "warning", + "info" + ], + "description": "'error': blocks optimization until resolved. 'warning': optimization works but effectiveness is reduced. 'info': suggestion for improvement." + }, + "message": { + "type": "string", + "description": "Human/agent-readable description of the issue and how to resolve it." + } + }, + "required": [ + "severity", + "message" + ], + "additionalProperties": true + } + }, + "notes": { + "type": "string", + "description": "Seller explanation of the readiness assessment, recommendations for improvement, or context about what the buyer needs to change." + } + }, + "required": [ + "status" + ], + "additionalProperties": true + }, + "conversion_tracking": { + "type": "object", + "description": "Conversion event tracking for this product. Presence indicates the product supports optimization_goals with kind: 'event'. Seller-level capabilities (supported event types, UID types, attribution windows) are declared in get_adcp_capabilities.", + "properties": { + "action_sources": { + "type": "array", + "description": "Action sources relevant to this product (e.g. a retail media product might have 'in_store' and 'website', while a display product might only have 'website')", + "items": { + "$ref": "#/$defs/ActionSource" + }, + "minItems": 1 + }, + "supported_targets": { + "type": "array", + "description": "Target kinds available for event goals on this product. Values match target.kind on the optimization goal. cost_per: target cost per conversion event. per_ad_spend: target return on ad spend (requires value_field on event sources). maximize_value: maximize total conversion value without a specific ratio target (requires value_field). Only these target kinds are accepted \u2014 goals with unlisted target kinds will be rejected. A goal without a target implicitly maximizes conversion count within budget \u2014 no declaration needed for that mode. When omitted, buyers can still set target-less event goals.", + "items": { + "type": "string", + "enum": [ + "cost_per", + "per_ad_spend", + "maximize_value" + ] + }, + "minItems": 1 + }, + "platform_managed": { + "type": "boolean", + "description": "Whether the seller provides its own always-on measurement (e.g. Amazon sales attribution for Amazon advertisers). When true, sync_event_sources response will include seller-managed event sources with managed_by='seller'." + } + }, + "additionalProperties": true + }, + "catalog_match": { + "type": "object", + "description": "When the buyer provides a catalog on get_products, indicates which catalog items are eligible for this product. Only present for products where catalog matching is relevant (e.g., sponsored product listings, job boards, hotel ads).", + "properties": { + "matched_gtins": { + "type": "array", + "description": "GTINs from the buyer's catalog that are eligible on this product's inventory. Standard GTIN formats (GTIN-8 through GTIN-14). Only present for product-type catalogs with GTIN matching.", + "items": { + "type": "string", + "pattern": "^[0-9]{8,14}$" + } + }, + "matched_ids": { + "type": "array", + "description": "Item IDs from the buyer's catalog that matched this product's inventory. The ID type depends on the catalog type and content_id_type (e.g., SKUs for product catalogs, job_ids for job catalogs, offering_ids for offering catalogs).", + "items": { + "type": "string" + } + }, + "matched_count": { + "type": "integer", + "description": "Number of catalog items that matched this product's inventory.", + "minimum": 0 + }, + "submitted_count": { + "type": "integer", + "description": "Total catalog items evaluated from the buyer's catalog.", + "minimum": 0 + } + }, + "required": [ + "submitted_count" + ] + }, + "brief_relevance": { + "type": "string", + "description": "Explanation of why this product matches the brief (only included when brief is provided)" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "Expiration timestamp. After this time, the product may no longer be available for purchase and create_media_buy may reject packages referencing it." + }, + "product_card": { + "type": "object", + "description": "Optional standard visual card for displaying this product in user interfaces (catalog browsers, dashboards, agent UIs). Distinct from `format` \u2014 product_card describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection. Receivers render the card directly from these fields.", + "properties": { + "image": { + "title": "Image Asset", + "description": "Hero image for the card. Recommended ~300x400 (4:3 portrait) for the standard card layout; receivers may scale.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "title": { + "type": "string", + "description": "Card title (typically the product name).", + "maxLength": 60 + }, + "description": { + "type": "string", + "description": "Short descriptive blurb shown below the title.", + "maxLength": 200 + }, + "price_label": { + "type": "string", + "description": "Formatted price or pricing summary (e.g., 'From $5 CPM', 'Auction floor $0.50 CPC'). Free-text \u2014 receivers render verbatim.", + "maxLength": 30 + }, + "cta_label": { + "type": "string", + "description": "Call-to-action button label (e.g., 'View details', 'Get proposal').", + "maxLength": 25 + } + }, + "additionalProperties": true + }, + "product_card_detailed": { + "type": "object", + "description": "Optional detailed card with hero + carousel + structured specifications, for rich product presentation (media-kit-style pages, full product detail views). Distinct from `format` \u2014 describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection.", + "properties": { + "hero_image": { + "title": "Image Asset", + "description": "Primary hero image at the top of the detailed view.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "carousel_images": { + "type": "array", + "description": "Additional images for a swipeable carousel below the hero.", + "items": { + "title": "Image Asset", + "description": "Image asset with URL and dimensions", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + } + }, + "title": { + "type": "string", + "description": "Page title (typically the product name)." + }, + "description": { + "type": "string", + "description": "Full descriptive copy. Markdown allowed in client renderers that support it; otherwise treat as plain text." + }, + "specifications": { + "type": "array", + "description": "Structured key/value specifications (e.g., 'Aspect ratio: 9:16', 'Duration: 30s'). Each item is a labeled fact about the product.", + "items": { + "type": "object", + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string", + "maxLength": 60 + }, + "value": { + "type": "string", + "maxLength": 200 + } + }, + "additionalProperties": true + } + }, + "price_label": { + "type": "string", + "description": "Formatted price or pricing summary." + }, + "cta_label": { + "type": "string", + "description": "Call-to-action button label." + } + }, + "additionalProperties": true + }, + "collections": { + "type": "array", + "description": "Collections available in this product. Each entry references collections declared in an adagents.json by domain and collection ID. Buyers resolve full collection objects from the referenced adagents.json.", + "items": { + "title": "Collection Selector", + "description": "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.", + "type": "object", + "properties": { + "publisher_domain": { + "type": "string", + "description": "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.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "collection_ids": { + "type": "array", + "description": "Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "publisher_domain", + "collection_ids" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "collection_targeting_allowed": { + "type": "boolean", + "default": false, + "description": "Whether buyers can target a subset of this product's collections. When false (default), the product is a bundle \u2014 buyers get all listed collections. When true, buyers can select specific collections in the media buy." + }, + "installments": { + "type": "array", + "description": "Specific installments included in this product. Each installment references its parent collection via collection_id when the product spans multiple collections. When absent with collections present, the product covers the collections broadly (run-of-collection).", + "items": { + "title": "Installment", + "description": "A single bookable unit within a collection \u2014 one episode, issue, event, or rotation period. The parent collection's kind indicates how to interpret each installment: TV/podcast episodes, print issues, live event airings, newsletter editions, or DOOH rotation periods. Installments inherit collection-level fields they don't override: content_rating defaults to the collection's baseline, guest_talent is additive to the collection's recurring talent, and topics add context beyond the collection's genre.", + "type": "object", + "properties": { + "installment_id": { + "type": "string", + "description": "Unique identifier for this installment within the collection" + }, + "collection_id": { + "type": "string", + "description": "Parent collection reference. Required when the product spans multiple collections. Maps to a collection_id declared in one of the publishers' adagents.json files referenced by the product's collection selectors." + }, + "name": { + "type": "string", + "description": "Installment title" + }, + "season": { + "type": "string", + "description": "Season identifier (e.g., '1', '2024', 'spring_2026')" + }, + "installment_number": { + "type": "string", + "description": "Installment number within the season (e.g., '3', '47')" + }, + "scheduled_at": { + "type": "string", + "format": "date-time", + "description": "When the installment airs or publishes (ISO 8601)" + }, + "status": { + "$ref": "#/$defs/InstallmentStatus" + }, + "duration_seconds": { + "type": "integer", + "minimum": 0, + "description": "Expected duration of the installment in seconds" + }, + "flexible_end": { + "type": "boolean", + "description": "Whether the end time is approximate (live events, sports)" + }, + "valid_until": { + "type": "string", + "format": "date-time", + "description": "When this installment data expires and should be re-queried. Agents should re-query before committing budget to products with tentative installments." + }, + "content_rating": { + "title": "Content Rating", + "description": "Installment-specific content rating. Overrides the collection's baseline content_rating when present.", + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/ContentRatingSystem" + }, + "rating": { + "type": "string", + "description": "Rating value within the system (e.g., 'TV-PG', 'R', 'explicit')" + } + }, + "required": [ + "system", + "rating" + ], + "additionalProperties": true + }, + "topics": { + "type": "array", + "description": "Content topics for this installment. Uses the same taxonomy as the collection's genre_taxonomy when present. Enables installment-level brand safety evaluation beyond content_rating.", + "items": { + "type": "string" + } + }, + "special": { + "title": "Special", + "description": "Installment-specific event context. When present, this installment is anchored to a real-world event. Overrides the collection-level special when present.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the event (e.g., 'Olympics 2028', 'Super Bowl LXI')" + }, + "category": { + "$ref": "#/$defs/SpecialCategory" + }, + "starts": { + "type": "string", + "format": "date-time", + "description": "When the event starts (ISO 8601)" + }, + "ends": { + "type": "string", + "format": "date-time", + "description": "When the event ends (ISO 8601). Omit for single-day events." + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "guest_talent": { + "type": "array", + "description": "Installment-specific guests and talent. Additive to the collection's recurring talent.", + "items": { + "title": "Talent", + "description": "A person associated with a collection or installment, with an optional link to their brand.json identity", + "type": "object", + "properties": { + "role": { + "$ref": "#/$defs/TalentRole" + }, + "name": { + "type": "string", + "description": "Person's name as credited on the collection" + }, + "brand_url": { + "type": "string", + "format": "uri", + "description": "URL to this person's brand.json entry. Enables buyer agents to evaluate the talent's brand identity and associations." + } + }, + "required": [ + "role", + "name" + ], + "additionalProperties": true + } + }, + "ad_inventory": { + "title": "Ad Inventory Configuration", + "description": "Break-based ad inventory for this installment. For non-break formats (host reads, integrations), use product placements.", + "type": "object", + "properties": { + "expected_breaks": { + "type": "integer", + "minimum": 0, + "description": "Number of planned ad breaks in the installment" + }, + "total_ad_seconds": { + "type": "integer", + "minimum": 0, + "description": "Total seconds of ad time across all breaks" + }, + "max_ad_duration_seconds": { + "type": "integer", + "minimum": 1, + "description": "Maximum duration in seconds for a single ad within a break. Buyers need this to know whether their creative fits." + }, + "unplanned_breaks": { + "type": "boolean", + "description": "Whether ad breaks are dynamic and driven by live conditions (sports timeouts, election coverage). When false, all breaks are pre-defined." + }, + "supported_formats": { + "type": "array", + "description": "Ad format types supported in breaks (e.g., 'video', 'audio', 'display')", + "items": { + "type": "string" + } + } + }, + "required": [ + "expected_breaks" + ], + "additionalProperties": true + }, + "deadlines": { + "title": "Installment Deadlines", + "description": "Booking, cancellation, and material submission deadlines for this installment. Present when the installment has time-sensitive inventory that requires advance commitment or material delivery.", + "type": "object", + "properties": { + "booking_deadline": { + "type": "string", + "format": "date-time", + "description": "Last date/time to book a placement in this installment (ISO 8601). After this point, the seller will not accept new bookings." + }, + "cancellation_deadline": { + "type": "string", + "format": "date-time", + "description": "Last date/time to cancel without penalty (ISO 8601). Cancellations after this point may incur fees per the seller's terms." + }, + "material_deadlines": { + "type": "array", + "description": "Stages for creative material submission. Items MUST be in chronological order by due_at (earliest first). Typical pattern: 'draft' for raw materials the seller will process, 'final' for production-ready assets. Print example: draft artwork then press-ready PDF. Influencer example: talking points then approved script.", + "items": { + "title": "Material Deadline", + "description": "A deadline for creative material submission. Sellers declare stages to distinguish draft materials (e.g., talking points, raw artwork) from production-ready assets (e.g., approved scripts, press-ready PDFs).", + "type": "object", + "properties": { + "stage": { + "type": "string", + "description": "Submission stage identifier. Use 'draft' for materials that need seller processing and 'final' for production-ready assets. Sellers may define additional stages.", + "examples": [ + "draft", + "final" + ] + }, + "due_at": { + "type": "string", + "format": "date-time", + "description": "When materials for this stage are due (ISO 8601)" + }, + "label": { + "type": "string", + "description": "What the seller needs at this stage (e.g., 'Talking points and brand guidelines', 'Press-ready PDF with bleed')" + } + }, + "required": [ + "stage", + "due_at" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "minProperties": 1, + "additionalProperties": true + }, + "derivative_of": { + "type": "object", + "description": "When this installment is a clip, highlight, or recap derived from a full installment. The source installment_id must reference an installment within the same response.", + "properties": { + "installment_id": { + "type": "string", + "description": "The source installment this content is derived from" + }, + "type": { + "$ref": "#/$defs/DerivativeType" + } + }, + "required": [ + "installment_id", + "type" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "installment_id" + ], + "additionalProperties": true + } + }, + "enforced_policies": { + "type": "array", + "description": "Registry policy IDs the seller enforces for this product. Enforcement level comes from the policy registry. Buyers can filter products by required policies.", + "items": { + "type": "string" + } + }, + "trusted_match": { + "type": "object", + "description": "Trusted Match Protocol capabilities for this product. When present, the product supports real-time contextual and/or identity matching via TMP. Buyers use this to determine what response types the publisher can accept and whether brands can be selected dynamically at match time.", + "properties": { + "context_match": { + "type": "boolean", + "description": "Whether this product supports Context Match requests. When true, the publisher's TMP router will send context match requests to registered providers for this product's inventory.", + "default": true + }, + "identity_match": { + "type": "boolean", + "description": "Whether this product supports Identity Match requests. When true, the publisher's TMP router will send identity match requests to evaluate user eligibility.", + "default": false + }, + "response_types": { + "type": "array", + "description": "What the publisher can accept back from context match.", + "items": { + "$ref": "#/$defs/TMPResponseType" + }, + "minItems": 1, + "default": [ + "activation" + ] + }, + "dynamic_brands": { + "type": "boolean", + "description": "Whether the buyer can select a brand at match time. When false (default), the brand must be specified on the media buy/package. When true, the buyer's offer can include any brand \u2014 the publisher applies approval rules at match time. Enables multi-brand agreements where the holding company or buyer agent selects brand based on context.", + "default": false + }, + "providers": { + "type": "array", + "description": "TMP providers integrated with this product's inventory. Each entry identifies a provider by agent_url (from the registry) and declares what match types it supports for this product. The product-level context_match and identity_match booleans declare what the product supports overall; the per-provider booleans declare which provider handles each match type. Enables buyer discovery: 'find products where a specific provider does context matching.'", + "items": { + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "Provider's agent URL from the registry. Canonical identifier for this TMP provider." + }, + "context_match": { + "type": "boolean", + "description": "Whether this provider handles context match for this product.", + "default": false + }, + "identity_match": { + "type": "boolean", + "description": "Whether this provider handles identity match for this product.", + "default": false + }, + "countries": { + "type": "array", + "description": "ISO 3166-1 alpha-2 country codes this provider serves for identity match. The router uses this to select the correct regional provider based on the request's country field. Required when identity_match is true.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "uid_types": { + "type": "array", + "description": "Identity types this regional provider can resolve. The router filters providers whose uid_types includes the request's uid_type. Required when identity_match is true.", + "items": { + "$ref": "#/$defs/UIDType" + }, + "minItems": 1 + } + }, + "required": [ + "agent_url" + ], + "if": { + "properties": { + "identity_match": { + "const": true + } + }, + "required": [ + "identity_match" + ] + }, + "then": { + "required": [ + "agent_url", + "countries", + "uid_types" + ] + }, + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "context_match" + ], + "additionalProperties": true + }, + "material_submission": { + "type": "object", + "description": "Instructions for submitting physical creative materials (print, static OOH, cinema). Present only for products requiring physical delivery outside the digital creative assignment flow. Buyer agents MUST validate url and email domains against the seller's known domains (from adagents.json) before submitting materials. Never auto-submit without human confirmation.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL for uploading or submitting physical creative materials" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address for creative material submission" + }, + "instructions": { + "type": "string", + "description": "Human-readable instructions for material submission (file naming conventions, shipping address, etc.)", + "maxLength": 2000 + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "minProperties": 1, + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "product_id", + "name", + "description", + "publisher_properties", + "delivery_type", + "pricing_options", + "reporting_capabilities" + ], + "allOf": [ + { + "description": "Products with package-level signal targeting options or rules MUST declare the signal targeting surface explicitly.", + "if": { + "anyOf": [ + { + "required": [ + "signal_targeting_options" + ] + }, + { + "required": [ + "signal_targeting_rules" + ] + } + ] + }, + "then": { + "properties": { + "signal_targeting_allowed": { + "const": true + } + }, + "required": [ + "signal_targeting_allowed" + ] + } + } + ], + "anyOf": [ + { + "title": "Legacy Product (named-format reference)", + "description": "Product references one or more named formats by structured format_id ({ agent_url, id }). This is the legacy named-format path; it remains supported through 4.x.", + "required": [ + "format_ids" + ] + }, + { + "title": "3.1+ Product (format-option declarations)", + "description": "Product carries one or more inline ProductFormatDeclarations, each narrowing a canonical format. This is the 3.1+ format-option path introduced by RFC #3305. A single-element `format_options` array is the 90% case; multi-element arrays declare that the product accepts any of the listed format options.", + "required": [ + "format_options" + ] + } + ], + "additionalProperties": true + } + }, + "extensions": { + "type": "object", + "description": "Bundled platform-extension definitions referenced by any product in `products`. Keyed by `@` (e.g., `https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel@sha256:abc...`). When present, lets buyers resolve `platform_extensions` references on product format declarations without a separate fetch. Buyer SDKs cache by URI@digest; subsequent get_products responses MAY omit definitions the buyer already has cached and rely on the digest match. Each value is an extension definition with `extends` (the canonical concept it extends, e.g., `tracking`), `fields` (the schema for additional fields the extension contributes), `version`, and optional `description`.", + "patternProperties": { + "^https?://[^@]+@sha256:[a-f0-9]{64}$": { + "type": "object", + "required": [ + "extends", + "fields" + ], + "properties": { + "extends": { + "type": "string", + "description": "Canonical concept this extension extends (e.g., `tracking`, `cta_vocabulary`, `destinations`, `placement`)." + }, + "fields": { + "type": "object", + "description": "JSON Schema fragment declaring the additional fields this extension contributes." + }, + "version": { + "type": "string", + "description": "Semantic version of the extension definition. Distinct from the digest \u2014 version is human-readable; digest is the integrity check." + }, + "description": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "proposals": { + "type": "array", + "description": "Optional array of proposed media plans with budget allocations across products. Publishers include proposals when they can provide strategic guidance based on the brief. Proposals are actionable - buyers can refine them via follow-up get_products calls within the same session, or execute them directly via create_media_buy.", + "items": { + "title": "Proposal", + "description": "A proposed media plan with budget allocations across products. Represents the publisher's strategic recommendation for how to structure a campaign based on the brief. Proposals are actionable - buyers can execute them directly via create_media_buy by providing the proposal_id.", + "type": "object", + "properties": { + "proposal_id": { + "type": "string", + "description": "Unique identifier for this proposal. Used to execute it via create_media_buy.", + "maxLength": 255 + }, + "name": { + "type": "string", + "description": "Human-readable name for this media plan proposal", + "maxLength": 500 + }, + "description": { + "type": "string", + "description": "Explanation of the proposal strategy and what it achieves", + "maxLength": 2000 + }, + "allocations": { + "type": "array", + "description": "Budget allocations across products. Allocation percentages MUST sum to 100. Publishers are responsible for ensuring the sum equals 100; buyers SHOULD validate this before execution.", + "items": { + "title": "Product Allocation", + "description": "A budget allocation for a specific product within a proposal. Percentages across all allocations in a proposal should sum to 100.", + "type": "object", + "properties": { + "product_id": { + "type": "string", + "description": "ID of the product (must reference a product in the products array)" + }, + "allocation_percentage": { + "type": "number", + "description": "Percentage of total budget allocated to this product (0-100)", + "minimum": 0, + "maximum": 100 + }, + "pricing_option_id": { + "type": "string", + "description": "Recommended pricing option ID from the product's pricing_options array" + }, + "rationale": { + "type": "string", + "description": "Explanation of why this product and allocation are recommended" + }, + "sequence": { + "type": "integer", + "description": "Optional ordering hint for multi-line-item plans (1-based)", + "minimum": 1 + }, + "tags": { + "type": "array", + "description": "Categorical tags for this allocation (e.g., 'desktop', 'german', 'mobile') - useful for grouping/filtering allocations by dimension", + "items": { + "type": "string" + } + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "Recommended flight start date/time for this allocation in ISO 8601 format. Allows publishers to propose per-flight scheduling within a proposal. When omitted, the allocation applies to the full campaign date range." + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Recommended flight end date/time for this allocation in ISO 8601 format. Allows publishers to propose per-flight scheduling within a proposal. When omitted, the allocation applies to the full campaign date range." + }, + "daypart_targets": { + "type": "array", + "description": "Recommended time windows for this allocation in spot-plan proposals.", + "items": { + "title": "Daypart Target", + "description": "A time window for daypart targeting. Specifies days of week and an hour range. start_hour is inclusive, end_hour is exclusive (e.g., 6-10 = 6:00am to 10:00am). Follows the Google Ads AdScheduleInfo / DV360 DayPartTargeting pattern.", + "type": "object", + "properties": { + "days": { + "type": "array", + "description": "Days of week this window applies to. Use multiple days for compact targeting (e.g., monday-friday in one object).", + "items": { + "$ref": "#/$defs/DayOfWeek" + }, + "minItems": 1 + }, + "start_hour": { + "type": "integer", + "description": "Start hour (inclusive), 0-23 in 24-hour format. 0 = midnight, 6 = 6:00am, 18 = 6:00pm.", + "minimum": 0, + "maximum": 23 + }, + "end_hour": { + "type": "integer", + "description": "End hour (exclusive), 1-24 in 24-hour format. 10 = 10:00am, 24 = midnight. Must be greater than start_hour.", + "minimum": 1, + "maximum": 24 + }, + "label": { + "type": "string", + "description": "Optional human-readable name for this time window (e.g., 'Morning Drive', 'Prime Time')" + } + }, + "required": [ + "days", + "start_hour", + "end_hour" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "forecast": { + "title": "Delivery Forecast", + "description": "Forecasted delivery metrics for this allocation", + "type": "object", + "properties": { + "points": { + "type": "array", + "description": "Forecasted delivery data points. For spend curves (default), points at ascending budget levels show how metrics scale with spend. For availability forecasts, points represent total available inventory independent of budget. See forecast_range_unit for interpretation.", + "items": { + "title": "Forecast Point", + "description": "A forecast data point. When budget is present, the point pairs a spend level with expected delivery \u2014 multiple points at ascending budgets form a curve. When budget is omitted, the point represents total available inventory for the requested targeting and dates, independent of spend.", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 128, + "description": "Human-readable name for this forecast point. Required when forecast_range_unit is 'package' so buyer agents can identify and reference individual packages. Optional for other forecast types.", + "examples": [ + "Primetime", + "Morning Drive", + "Large Format Transit" + ] + }, + "budget": { + "type": "number", + "description": "Budget amount for this forecast point. Required for spend curves; omit for availability forecasts where the metrics represent total available inventory. For allocation-level forecasts, this is the absolute budget for that allocation (not the percentage). For proposal-level forecasts, this is the total proposal budget. When omitted, use metrics.spend to express the estimated cost of the available inventory.", + "minimum": 0 + }, + "metrics": { + "type": "object", + "description": "Forecasted metric values. Keys are forecastable-metric enum values for delivery/engagement or event-type enum values for outcomes. Values are ForecastRange objects (low/mid/high). Use { \"mid\": value } for point estimates. When budget is present, these are the expected metrics at that spend level. When budget is omitted, these represent total available inventory \u2014 use spend to express the estimated cost. Additional keys beyond the documented properties are allowed for event-type values (purchase, lead, app_install, etc.).", + "properties": { + "audience_size": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "reach": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "frequency": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "clicks": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "spend": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "completed_views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "grps": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "engagements": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "follows": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "saves": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "profile_visits": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "measured_impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "downloads": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "plays": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + }, + "additionalProperties": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + } + }, + "required": [ + "metrics" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "forecast_range_unit": { + "$ref": "#/$defs/ForecastRangeUnit" + }, + "method": { + "$ref": "#/$defs/ForecastMethod" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for monetary values in this forecast (spend, budget)" + }, + "demographic_system": { + "$ref": "#/$defs/DemographicSystem" + }, + "demographic": { + "type": "string", + "description": "Target demographic code within the specified demographic_system. For Nielsen: P18-49, M25-54, W35+. For BARB: ABC1 Adults, 16-34. For AGF: E 14-49.", + "examples": [ + "P18-49", + "A25-54", + "W35+", + "M18-34" + ] + }, + "measurement_source": { + "type": "string", + "maxLength": 64, + "pattern": "^[a-z0-9_]+$", + "description": "Third-party measurement provider whose data was used to produce this forecast. Distinct from demographic_system, which specifies demographic notation \u2014 measurement_source identifies whose data produced the forecast numbers. Should be present when measured_impressions is used. Lowercase slug format.", + "examples": [ + "nielsen", + "videoamp", + "comscore", + "geopath", + "barb", + "agf", + "oztam", + "kantar", + "barc", + "route", + "rajar", + "triton" + ] + }, + "reach_unit": { + "$ref": "#/$defs/ReachUnit" + }, + "generated_at": { + "type": "string", + "format": "date-time", + "description": "When this forecast was computed" + }, + "valid_until": { + "type": "string", + "format": "date-time", + "description": "When this forecast expires. After this time, the forecast should be refreshed. Forecast expiry does not affect proposal executability." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "points", + "method", + "currency" + ], + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "product_id", + "allocation_percentage" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "proposal_status": { + "title": "Proposal Status", + "description": "Lifecycle status of this proposal. When absent, the proposal is ready to buy (backward compatible). 'draft' means indicative pricing \u2014 finalize via refine before purchasing. 'committed' means firm pricing with inventory reserved until expires_at.", + "type": "string", + "enum": [ + "draft", + "committed" + ], + "enumDescriptions": { + "draft": "Indicative pricing and availability. The buyer can compare and plan but must finalize before purchasing. Use the 'finalize' refine action to request firm pricing.", + "committed": "Firm pricing with inventory reserved. The buyer can execute this proposal via create_media_buy before expires_at. After expires_at, the hold lapses and the buyer must re-finalize or re-discover." + } + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When this proposal expires and can no longer be executed. For draft proposals, indicates when indicative pricing becomes stale. For committed proposals, indicates when the inventory hold lapses \u2014 the buyer must call create_media_buy before this time." + }, + "insertion_order": { + "title": "Insertion Order", + "description": "Formal insertion order attached to a committed proposal. Present when the seller requires a signed agreement before the media buy can proceed. The buyer references the io_id in io_acceptance on create_media_buy.", + "type": "object", + "properties": { + "io_id": { + "type": "string", + "description": "Unique identifier for this insertion order. Referenced by io_acceptance on create_media_buy.", + "maxLength": 255 + }, + "terms": { + "type": "object", + "description": "Summary fields echoed from the committed proposal for agent verification. Buyer agents use these to confirm the IO matches what was negotiated before a human signs. These are read-only summaries, not negotiation surfaces \u2014 deal terms live on products and packages.", + "properties": { + "advertiser": { + "type": "string", + "description": "Advertiser name or identifier", + "maxLength": 500 + }, + "publisher": { + "type": "string", + "description": "Publisher name or identifier", + "maxLength": 500 + }, + "total_budget": { + "type": "object", + "description": "Total committed budget", + "properties": { + "amount": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "minLength": 3, + "maxLength": 3 + } + }, + "required": [ + "amount", + "currency" + ] + }, + "flight_start": { + "type": "string", + "format": "date-time", + "description": "Campaign start date" + }, + "flight_end": { + "type": "string", + "format": "date-time", + "description": "Campaign end date" + }, + "payment_terms": { + "type": "string", + "description": "Payment terms", + "enum": [ + "net_30", + "net_60", + "net_90", + "prepaid", + "due_on_receipt" + ] + } + }, + "additionalProperties": true + }, + "terms_url": { + "type": "string", + "format": "uri", + "description": "URL to a human-readable document containing the full insertion order terms" + }, + "signing_url": { + "type": "string", + "format": "uri", + "description": "URL to an electronic signing service (e.g., DocuSign) for human signature workflows. When present, a human must sign before the buyer agent can proceed with create_media_buy." + }, + "requires_signature": { + "type": "boolean", + "description": "Whether the buyer must accept this IO before creating a media buy. When true, create_media_buy requires an io_acceptance referencing this io_id." + } + }, + "required": [ + "io_id", + "requires_signature" + ], + "additionalProperties": true + }, + "total_budget_guidance": { + "type": "object", + "description": "Optional budget guidance for this proposal", + "properties": { + "min": { + "type": "number", + "description": "Minimum recommended budget", + "minimum": 0 + }, + "recommended": { + "type": "number", + "description": "Recommended budget for optimal performance", + "minimum": 0 + }, + "max": { + "type": "number", + "description": "Maximum budget before diminishing returns", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code" + } + }, + "additionalProperties": true + }, + "brief_alignment": { + "type": "string", + "description": "Explanation of how this proposal aligns with the campaign brief", + "maxLength": 2000 + }, + "forecast": { + "title": "Delivery Forecast", + "description": "Aggregate forecasted delivery metrics for the entire proposal. When both proposal-level and allocation-level forecasts are present, the proposal-level forecast is authoritative for total delivery estimation.", + "type": "object", + "properties": { + "points": { + "type": "array", + "description": "Forecasted delivery data points. For spend curves (default), points at ascending budget levels show how metrics scale with spend. For availability forecasts, points represent total available inventory independent of budget. See forecast_range_unit for interpretation.", + "items": { + "title": "Forecast Point", + "description": "A forecast data point. When budget is present, the point pairs a spend level with expected delivery \u2014 multiple points at ascending budgets form a curve. When budget is omitted, the point represents total available inventory for the requested targeting and dates, independent of spend.", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 128, + "description": "Human-readable name for this forecast point. Required when forecast_range_unit is 'package' so buyer agents can identify and reference individual packages. Optional for other forecast types.", + "examples": [ + "Primetime", + "Morning Drive", + "Large Format Transit" + ] + }, + "budget": { + "type": "number", + "description": "Budget amount for this forecast point. Required for spend curves; omit for availability forecasts where the metrics represent total available inventory. For allocation-level forecasts, this is the absolute budget for that allocation (not the percentage). For proposal-level forecasts, this is the total proposal budget. When omitted, use metrics.spend to express the estimated cost of the available inventory.", + "minimum": 0 + }, + "metrics": { + "type": "object", + "description": "Forecasted metric values. Keys are forecastable-metric enum values for delivery/engagement or event-type enum values for outcomes. Values are ForecastRange objects (low/mid/high). Use { \"mid\": value } for point estimates. When budget is present, these are the expected metrics at that spend level. When budget is omitted, these represent total available inventory \u2014 use spend to express the estimated cost. Additional keys beyond the documented properties are allowed for event-type values (purchase, lead, app_install, etc.).", + "properties": { + "audience_size": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "reach": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "frequency": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "clicks": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "spend": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "completed_views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "grps": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "engagements": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "follows": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "saves": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "profile_visits": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "measured_impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "downloads": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "plays": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + }, + "additionalProperties": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + } + }, + "required": [ + "metrics" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "forecast_range_unit": { + "$ref": "#/$defs/ForecastRangeUnit" + }, + "method": { + "$ref": "#/$defs/ForecastMethod" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for monetary values in this forecast (spend, budget)" + }, + "demographic_system": { + "$ref": "#/$defs/DemographicSystem" + }, + "demographic": { + "type": "string", + "description": "Target demographic code within the specified demographic_system. For Nielsen: P18-49, M25-54, W35+. For BARB: ABC1 Adults, 16-34. For AGF: E 14-49.", + "examples": [ + "P18-49", + "A25-54", + "W35+", + "M18-34" + ] + }, + "measurement_source": { + "type": "string", + "maxLength": 64, + "pattern": "^[a-z0-9_]+$", + "description": "Third-party measurement provider whose data was used to produce this forecast. Distinct from demographic_system, which specifies demographic notation \u2014 measurement_source identifies whose data produced the forecast numbers. Should be present when measured_impressions is used. Lowercase slug format.", + "examples": [ + "nielsen", + "videoamp", + "comscore", + "geopath", + "barb", + "agf", + "oztam", + "kantar", + "barc", + "route", + "rajar", + "triton" + ] + }, + "reach_unit": { + "$ref": "#/$defs/ReachUnit" + }, + "generated_at": { + "type": "string", + "format": "date-time", + "description": "When this forecast was computed" + }, + "valid_until": { + "type": "string", + "format": "date-time", + "description": "When this forecast expires. After this time, the forecast should be refreshed. Forecast expiry does not affect proposal executability." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "points", + "method", + "currency" + ], + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "proposal_id", + "name", + "allocations" + ], + "additionalProperties": true + } + }, + "errors": { + "type": "array", + "description": "Task-specific errors and warnings (e.g., product filtering issues)", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "property_list_applied": { + "type": "boolean", + "description": "[AdCP 3.0] Indicates whether property_list filtering was applied. True if the agent filtered products based on the provided property_list. Absent or false if property_list was not provided or not supported by this agent." + }, + "catalog_applied": { + "type": "boolean", + "description": "Whether the seller filtered results based on the provided catalog. True if the seller matched catalog items against its inventory. Absent or false if no catalog was provided or the seller does not support catalog matching." + }, + "refinement_applied": { + "type": "array", + "description": "Seller's response to each change request in the refine array, matched by position. Each entry acknowledges whether the corresponding ask was applied, partially applied, or unable to be fulfilled. MUST contain the same number of entries in the same order as the request's refine array. Only present when the request used buying_mode: 'refine'. Each entry MUST echo the request entry's scope and \u2014 for product and proposal scopes \u2014 the matching id field (product_id or proposal_id), so orchestrators can cross-validate alignment.", + "items": { + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "properties": { + "scope": { + "type": "string", + "const": "request", + "description": "Echoes scope 'request' from the corresponding refine entry." + }, + "status": { + "type": "string", + "enum": [ + "applied", + "partial", + "unable" + ], + "description": "'applied': the ask was fulfilled. 'partial': the ask was partially fulfilled \u2014 see notes for details. 'unable': the seller could not fulfill the ask \u2014 see notes for why." + }, + "notes": { + "type": "string", + "description": "Seller explanation of what was done, what couldn't be done, or why. Recommended when status is 'partial' or 'unable'." + } + }, + "required": [ + "scope", + "status" + ], + "additionalProperties": false + }, + { + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Echoes scope 'product' from the corresponding refine entry." + }, + "product_id": { + "type": "string", + "description": "Echoes product_id from the corresponding refine entry." + }, + "status": { + "type": "string", + "enum": [ + "applied", + "partial", + "unable" + ], + "description": "'applied': the ask was fulfilled. 'partial': the ask was partially fulfilled \u2014 see notes for details. 'unable': the seller could not fulfill the ask \u2014 see notes for why." + }, + "notes": { + "type": "string", + "description": "Seller explanation of what was done, what couldn't be done, or why. Recommended when status is 'partial' or 'unable'." + } + }, + "required": [ + "scope", + "product_id", + "status" + ], + "additionalProperties": false + }, + { + "properties": { + "scope": { + "type": "string", + "const": "proposal", + "description": "Echoes scope 'proposal' from the corresponding refine entry." + }, + "proposal_id": { + "type": "string", + "description": "Echoes proposal_id from the corresponding refine entry." + }, + "status": { + "type": "string", + "enum": [ + "applied", + "partial", + "unable" + ], + "description": "'applied': the ask was fulfilled. 'partial': the ask was partially fulfilled \u2014 see notes for details. 'unable': the seller could not fulfill the ask \u2014 see notes for why." + }, + "notes": { + "type": "string", + "description": "Seller explanation of what was done, what couldn't be done, or why. Recommended when status is 'partial' or 'unable'." + } + }, + "required": [ + "scope", + "proposal_id", + "status" + ], + "additionalProperties": false + } + ] + } + }, + "incomplete": { + "type": "array", + "description": "Declares what the seller could not finish within the buyer's time_budget or due to internal limits. Each entry identifies a scope that is missing or partial. Absent when the response is fully complete.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "scope": { + "type": "string", + "enum": [ + "products", + "pricing", + "forecast", + "proposals", + "wholesale_feed" + ], + "description": "'products': not all inventory sources were searched. 'pricing': products returned but pricing is absent or unconfirmed. 'forecast': products returned but forecast data is absent. 'proposals': proposals were not generated or are incomplete. 'wholesale_feed': in wholesale mode, full feed enumeration could not complete in the time budget \u2014 symmetric with get_signals' 'wholesale_feed' scope so sellers have a precise way to declare wholesale-incomplete on the products surface." + }, + "description": { + "type": "string", + "description": "Human-readable explanation of what is missing and why." + }, + "estimated_wait": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "How much additional time would resolve this scope. Allows the buyer to decide whether to retry with a larger time_budget." + } + }, + "required": [ + "scope", + "description" + ], + "additionalProperties": false + } + }, + "filter_diagnostics": { + "type": "object", + "description": "Optional non-fatal diagnostic block describing how the request's `filters` narrowed the candidate set. Use this to disambiguate empty/small result lists between 'no inventory matches the brief' and 'a specific filter excluded everything', without breaking the filter-not-fail convention (sellers still silently exclude unmatched products; this block is observability, not error reporting). Sellers MAY populate this when meaningful narrowing occurred; buyers MAY use it for triage UX without depending on its presence. Counts only \u2014 products are not enumerated by name to avoid leaking competitive intelligence about adjacent campaigns or seller inventory. `total_candidates` and `excluded_by` are independently optional \u2014 sellers whose baseline candidate set size is sensitive MAY emit `excluded_by` without `total_candidates`, or vice versa.", + "properties": { + "semantics": { + "type": "string", + "enum": [ + "only", + "any", + "approximate" + ], + "description": "How `excluded_by[*].count` values are computed across multiple filters. `only`: counts products that would have been included if not for THIS filter alone (deterministic; the right value for 'which filter killed my result set' triage \u2014 recommended when feasible). `any`: counts products excluded by ANY filter (so multiple filters' counts may overlap and sum to more than `total_candidates`). `approximate`: sellers SHOULD use this when their pipeline can't cleanly attribute exclusions to a single filter. Buyers SHOULD inspect `semantics` before doing arithmetic on counts." + }, + "total_candidates": { + "type": "integer", + "description": "Number of products the seller considered before applying `filters`. Baseline for interpreting per-filter exclusion counts. Approximate \u2014 sellers MAY return a sampled or capped count when their candidate pool is large. Optional; sellers whose baseline candidate set size is sensitive (revealing market posture or competitive density) MAY omit this while still emitting `excluded_by`.", + "minimum": 0 + }, + "excluded_by": { + "type": "object", + "description": "Per-filter exclusion counts, keyed by the filter property name as it appears in the request's `filters` object (e.g., `required_metrics`, `required_vendor_metrics`, `required_geo_targeting`, `budget_range`). Values are objects carrying `count` and optional filter-specific detail. Only filters that actually narrowed the set need appear here; absence of a key means that filter did not exclude anything (or was not in the request).", + "additionalProperties": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of products excluded by this filter, interpreted per the parent `semantics` field.", + "minimum": 0 + }, + "values": { + "type": "array", + "description": "Optional list of the specific filter values that contributed to exclusions, when meaningful. For `required_metrics`: the metric names that excluded products (strings). For `required_vendor_metrics`: the vendor/metric pin entries (objects). Item shape is filter-specific; the schema admits string OR object items. Buyers without filter-specific knowledge SHOULD treat as opaque.", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + } + }, + "notes": { + "type": "string", + "description": "Optional human-readable note about why this filter narrowed the set (e.g., 'no products in this brief support DV viewability at the requested threshold')." + } + }, + "required": [ + "count" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": true, + "examples": [ + { + "semantics": "only", + "total_candidates": 47, + "excluded_by": { + "required_metrics": { + "count": 31, + "values": [ + "completed_views" + ] + }, + "required_geo_targeting": { + "count": 9 + }, + "budget_range": { + "count": 7 + } + } + } + ] + }, + "pagination": { + "title": "Pagination Response", + "description": "Standard cursor-based pagination metadata for list responses", + "type": "object", + "properties": { + "has_more": { + "type": "boolean", + "description": "Whether more results are available beyond this page" + }, + "cursor": { + "type": "string", + "description": "Opaque cursor to pass in the next request to fetch the next page. Only present when has_more is true." + }, + "total_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of items matching the query across all pages. Optional because not all backends can efficiently compute this." + } + }, + "required": [ + "has_more" + ], + "additionalProperties": false + }, + "wholesale_feed_version": { + "type": "string", + "description": "Opaque token representing the version of the wholesale product feed state used to compose this response. Sellers that implement conditional-fetch (if_wholesale_feed_version) MUST return this on every wholesale-mode response so buyers can cache and probe later. Buyers MUST treat the value as opaque \u2014 no format, no ordering, no inspection. The token is scope-keyed: it describes a version for the cache_scope declared on this response, NOT a global agent version. A buyer caches `(cache_scope, wholesale_feed_version)` pairs and presents the matching token on the next request. Scoping dimensions: (agent, buying_mode, filters, property_list, catalog) for cache_scope: 'public'; that tuple plus account_id for cache_scope: 'account'. pagination.cursor is NOT part of the scoping tuple. See specs/wholesale-feed-webhooks.md for the full cache layering model.", + "x-adcp-validation": { + "verifier_constraints": { + "required_for_wholesale_request": { + "task": "get_products", + "request_field": "buying_mode", + "equals": "wholesale" + } + }, + "spec": "specs/wholesale-feed-webhooks.md#consumer-pattern" + } + }, + "pricing_version": { + "type": "string", + "description": "Opaque token representing the version of the pricing layer, including product pricing_options and nested signal_targeting_options pricing_options. When the seller supports independent pricing versioning, pricing_version changes when prices move but wholesale_feed_version changes only when structure/metadata moves. Same cache_scope keying as wholesale_feed_version. Sellers not separating these MAY omit pricing_version and use wholesale_feed_version for both." + }, + "cache_scope": { + "type": "string", + "enum": [ + "public", + "account" + ], + "description": "Declares whether the wholesale_feed_version and pricing_version on this response describe a universal layer or an account-specific overlay. REQUIRED on every 3.1+ response (the 3.1 schema enforces this \u2014 the safety property of the two-layer cache model depends on it). 'public': this response describes the seller's published rate card; the buyer MAY dedupe under (agent, buying_mode, filters, property_list, catalog) without scoping by account. 'account': this response includes account-specific overrides; the buyer MUST cache the version under (agent, buying_mode, filters, property_list, catalog, account_id). When the request did NOT include `account`, the seller MUST return `cache_scope: 'public'`. When the request included `account`, the seller MUST return either: 'public' (this account prices off the public rate card \u2014 buyer dedupes) or 'account' (account-specific overrides exist \u2014 buyer caches under the account key). Sellers MAY return 'public' on an account-scoped request that previously had overrides \u2014 buyers SHOULD interpret this as a downgrade and drop their account-overlay for the (agent, filters, mode) tuple. Without schema-required cache_scope, a seller silently omitting the field on an account-scoped response would cause buyers to mis-key the cache and serve account-overlay payloads to other accounts \u2014 the canonical safety invariant of the entire cache layering model. **Backward-compatibility note for 3.1 validators:** SDKs that validate strictly against the 3.1 schema MUST select the validator based on the server-declared `adcp_version` (release-precision version negotiation, 3.1). For responses with `adcp_version` starting `3.0`, the 3.1 cache_scope-required constraint MUST be relaxed \u2014 pre-3.1 sellers correctly emit no cache_scope and remain conformant to their declared version. This is a tightening within 3.1, not a 3.0 break." + }, + "unchanged": { + "type": "boolean", + "const": true, + "description": "Present and `true` ONLY on wholesale-mode responses when the request carried if_wholesale_feed_version (and/or if_pricing_version) matching the seller's current version for the buyer's cache_scope, in which case products[] MUST be omitted; wholesale_feed_version (echoed), cache_scope (echoed), and pricing_version (echoed when used) MUST still be present. Buyers receiving unchanged: true MUST NOT mutate their local wholesale product mirror. **One shape per state:** sellers MUST NOT emit `unchanged: false` \u2014 the absence of the field IS the signal that the response carries products. Two shapes ({ unchanged: false, products: [...] } vs. { products: [...] }) for the same state would let some sellers always emit the field and some never would, creating an inconsistency the wire shouldn't carry." + }, + "sandbox": { + "type": "boolean", + "description": "When true, this response contains simulated data from sandbox mode." + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "if": { + "properties": { + "unchanged": { + "const": true + } + }, + "required": [ + "unchanged" + ] + }, + "then": { + "description": "Wholesale-feed unchanged response: products MUST be omitted; wholesale_feed_version and cache_scope MUST be present (echoed from the cached version).", + "required": [ + "wholesale_feed_version", + "cache_scope" + ], + "not": { + "required": [ + "products" + ] + } + }, + "else": { + "description": "Standard response: products[] MUST be present and cache_scope MUST declare the response's cache layer. wholesale_feed_version is required by the wholesale-mode contract and verifier constraints, but is not schema-required here because this response schema is shared by non-wholesale reads.", + "required": [ + "products", + "cache_scope" + ] + }, + "additionalProperties": true + }, + { + "title": "Get Products - Working", + "description": "Progress payload for active get_products task.", + "type": "object", + "properties": { + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Progress percentage of the search operation" + }, + "current_step": { + "type": "string", + "description": "Current step in the search process (e.g., 'searching_inventory', 'validating_availability')" + }, + "total_steps": { + "type": "integer", + "description": "Total number of steps in the search process" + }, + "step_number": { + "type": "integer", + "description": "Current step number (1-indexed)" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + { + "title": "Get Products - Input Required", + "description": "Payload when search is paused waiting for user clarification.", + "type": "object", + "properties": { + "reason": { + "type": "string", + "enum": [ + "CLARIFICATION_NEEDED", + "BUDGET_REQUIRED" + ], + "description": "Reason code indicating why input is needed" + }, + "partial_results": { + "type": "array", + "description": "Partial product results that may help inform the clarification", + "items": { + "title": "Product", + "description": "Represents available advertising inventory", + "type": "object", + "properties": { + "product_id": { + "type": "string", + "description": "Unique identifier for the product", + "x-entity": "product" + }, + "name": { + "type": "string", + "description": "Human-readable product name" + }, + "description": { + "type": "string", + "description": "Detailed description of the product and its inventory" + }, + "publisher_properties": { + "type": "array", + "description": "SDK implementers MUST enforce singular-only at runtime: each entry uses the singular `publisher_domain` form; the compact `publisher_domains[]` form is rejected on products. Codegen toolchains (json-schema-to-typescript, quicktype, datamodel-code-generator, openapi-typescript-codegen) often flatten the `allOf + $ref + not.required` restriction below poorly and may drop the rejection constraint silently, emitting an unrestricted type \u2014 runtime enforcement is the safety net. Publisher properties covered by this product. Buyers fetch actual property definitions from each publisher's adagents.json and validate agent authorization. Selection patterns mirror the authorization patterns in adagents.json for consistency. The compact `publisher_domains[]` form is reserved for adagents.json `authorized_agents[].publisher_properties[]` so that buy-side traffic-and-pricing flatteners can always treat each entry as exactly one publisher.", + "items": { + "allOf": [ + { + "title": "Publisher Property Selector", + "description": "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.", + "discriminator": { + "propertyName": "selection_type" + }, + "oneOf": [ + { + "type": "object", + "description": "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).", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` \u2014 exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "publisher_domains": { + "type": "array", + "description": "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.", + "items": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "minItems": 1, + "uniqueItems": true + }, + "selection_type": { + "type": "string", + "const": "all", + "description": "Discriminator indicating all properties from each addressed publisher are included" + } + }, + "required": [ + "selection_type" + ], + "allOf": [ + { + "not": { + "required": [ + "publisher_domain", + "publisher_domains" + ] + } + }, + { + "anyOf": [ + { + "required": [ + "publisher_domain" + ] + }, + { + "required": [ + "publisher_domains" + ] + } + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Select specific properties by ID. Single-publisher only \u2014 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.", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com').", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "selection_type": { + "type": "string", + "const": "by_id", + "description": "Discriminator indicating selection by specific property IDs" + }, + "property_ids": { + "type": "array", + "description": "Specific property IDs from the publisher's adagents.json", + "items": { + "title": "Property ID", + "description": "Identifier for a publisher property. Must be lowercase alphanumeric with underscores only.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "property", + "examples": [ + "cnn_ctv_app", + "homepage", + "mobile_ios", + "instagram" + ] + }, + "minItems": 1 + } + }, + "required": [ + "publisher_domain", + "selection_type", + "property_ids" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Select properties by tag membership. With `publisher_domains`, the same `property_tags` predicate is resolved against each listed publisher's adagents.json \u2014 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).", + "properties": { + "publisher_domain": { + "type": "string", + "description": "Domain where publisher's adagents.json is hosted (e.g., 'cnn.com'). XOR with `publisher_domains` \u2014 exactly one MUST be present on each `publisher_properties[]` entry; both-present and neither-present both fail validation.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "publisher_domains": { + "type": "array", + "description": "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.", + "items": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "minItems": 1, + "uniqueItems": true + }, + "selection_type": { + "type": "string", + "const": "by_tag", + "description": "Discriminator indicating selection by property tags" + }, + "property_tags": { + "type": "array", + "description": "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.", + "items": { + "title": "Property Tag", + "description": "Tag for categorizing publisher properties. Must be lowercase alphanumeric with underscores only.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "examples": [ + "ctv", + "premium", + "news", + "sports", + "meta_network", + "social_media" + ] + }, + "minItems": 1 + } + }, + "required": [ + "selection_type", + "property_tags" + ], + "allOf": [ + { + "not": { + "required": [ + "publisher_domain", + "publisher_domains" + ] + } + }, + { + "anyOf": [ + { + "required": [ + "publisher_domain" + ] + }, + { + "required": [ + "publisher_domains" + ] + } + ] + } + ], + "additionalProperties": true + } + ] + }, + { + "not": { + "required": [ + "publisher_domains" + ] + } + } + ] + }, + "minItems": 1 + }, + "channels": { + "type": "array", + "description": "Advertising channels this product is sold as. Products inherit from their properties' supported_channels but may narrow the scope. For example, a product covering YouTube properties might be sold as ['ctv'] even though those properties support ['olv', 'social', 'ctv'].", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "uniqueItems": true + }, + "format_ids": { + "type": "array", + "description": "Legacy named-format path: array of supported creative format IDs (structured format_id objects with agent_url and id). Products MUST carry `format_ids`, `format_options`, or BOTH; at least one is required. Named formats predate 3.1 and remain supported through the deprecation calendar (2027-Q4 floor / 2029-Q1 ceiling).\n\n**Dual emission**: A product MAY carry both `format_ids` and `format_options` simultaneously during the migration window. This is the recommended seller pattern \u2014 author once, SDK projects to both wire shapes via the [canonical mapping registry](/schemas/registries/v1-canonical-mapping.json), every buyer reads what it knows. When both are present, the two MUST refer to the SAME underlying format declaration (the `format_options[i]` narrows the canonical that the named format in `format_ids[i]` resolves to via the registry / explicit `canonical` field). SDKs that derive both shapes from one source guarantee this invariant; SDKs that don't MUST treat divergence as a build error and refuse to emit. **Buyer rule**: when both are present, prefer `format_options`; treat `format_ids` as fallback for legacy-format buyers. **Non-projectable formats**: when a named format has no clean 3.1+ format-option projection (no registry entry, no explicit `canonical` declaration on the named format, no structural match), SDKs MUST NOT emit `format_options` for that product \u2014 only `format_ids` ships, and the product remains legacy-format-only until the seller adds an explicit `canonical` field or files a registry entry.", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + } + }, + "format_options": { + "type": "array", + "minItems": 1, + "description": "3.1+ format-option path: one or more inline format declarations the product accepts. Each element narrows a canonical format with parameters, slots, and platform_extensions. The 90% case is a single-element array (one canonical narrowed for the product). Multi-element use cases: a product that accepts EITHER a third-party-hosted creative (for example, externally served `html5`) OR an internal `display_tag`; a video product that accepts a hosted `video_hosted` upload OR a `video_vast` tag. Buyers pick which option they're shipping at `sync_creatives` time by aligning their manifest to the matching declaration's `format_kind` and slots.\n\nProducts MUST carry `format_ids`, `format_options`, or BOTH; at least one is required. See `format_ids` description for the dual-emission contract (same underlying declaration when both are present; SDK derives one from the other; buyers prefer `format_options` when both are present).\n\nWhen `placements[]` also declare `format_ids` or `format_options`, product-level formats are the upper bound for the sellable product. Placement-level formats narrow the product-wide accepted set for that placement; they MUST NOT introduce a format the product does not accept. Buyers compute the effective accepted set for a placement as the intersection of product-level and placement-level declarations. For format options, match publisher-declared options by `{ publisher_domain, format_option_id }`, match product-local options by `format_option_id` when `publisher_domain` is omitted, and otherwise match declarations with the same `format_kind` whose placement parameters narrow the product declaration. If a placement has no format declaration, it inherits the product-level formats.", + "items": { + "title": "Product Format Declaration", + "description": "Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, 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 \u2014 lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). v1 named formats remain supported through the deprecation cycle; v2 product-bound declarations are opt-in.\n\n**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 \u2014 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**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.", + "type": "object", + "required": [ + "format_kind", + "params" + ], + "discriminator": { + "propertyName": "format_kind" + }, + "properties": { + "format_option_id": { + "type": "string", + "description": "Stable identifier for this format declaration within its namespace. REQUIRED when the parent product's `format_options` contains multiple declarations sharing the same `format_kind` (so buyers can disambiguate which option a manifest targets via `manifest.format_option_ref`). SHOULD be set on EVERY `format_options[]` entry \u2014 not just when structurally required to break a `format_kind` collision \u2014 so V2-mental-model buyers can use the V2 authoring path (`PackageRequest.format_option_refs[]`, `creative-manifest.format_option_ref`) against the product. Publisher-catalog-backed options pair this with `publisher_domain`; product-local options omit `publisher_domain` and are selected by `format_option_id` within the target product. A product that ships without selectable `format_option_id` values on its `format_options[]` entries is structurally 3.1-conformant but is not V2-authorable: buyers fall back to v1 `format_ids[]` and lose the stable naming the V2 path was designed to provide. Sellers MUST reject V2 authoring against such products with `UNSUPPORTED_FEATURE` and `error.details.reason` set to `format_option_refs_not_published` per `package-request.json`. Format-internal (not a URI). Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + }, + "publisher_domain": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "description": "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." + }, + "display_name": { + "type": "string", + "description": "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 \u2014 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." + }, + "applies_to_channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "uniqueItems": true, + "description": "Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel \u2014 `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`." + }, + "seller_preference": { + "type": "string", + "enum": [ + "preferred", + "accepted", + "discouraged" + ], + "description": "Optional soft routing hint *within* a product's accepted set of formats \u2014 NOT an enforcement axis. `preferred` \u2014 seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` \u2014 supported on equal footing with other format_options (default when omitted); `discouraged` \u2014 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 \u2014 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." + }, + "canonical_formats_only": { + "type": "boolean", + "default": false, + "description": "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 \u2014 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` \u2014 explicit v2-only is more useful than silent absence." + }, + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, THIS seller's specific product declaration may not work as declared \u2014 even if the underlying canonical is stable. Use for beta runtime paths, forward-looking catalog entries the runtime doesn't yet honor, or experimental products where the seller wants buyer-side caution. Buyers reading `experimental: true` on a product declaration SHOULD prefer the legacy named-format path when a fallback exists for the same product (via `format_ids` on the parent product or via this declaration's `v1_format_ref`) and SHOULD validate via `validate_input` or a sandbox before routing production budget.\n\nIndependent of the canonical's own `experimental` flag \u2014 a stable canonical (e.g., `image`, `video_hosted`) can carry an experimental product declaration when the seller is shipping a new runtime path that isn't fully wired yet. Conversely, an experimental canonical (`sponsored_placement`, `responsive_creative`, `agent_placement`) MAY carry non-experimental product declarations where the seller's adopter contract is well-tested. Buyer SDKs SHOULD filter products with `experimental: true` from default views and offer an opt-in flag to surface them.\n\nReplaces the earlier `runtime_status` enum (`stable | preview | declared_only`) \u2014 same semantic ('use with caution') without the cognitive overhead of two stability axes." + }, + "format_shape": { + "type": "string", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, \u2026). Non-canonical values valid (validators MAY soft-warn) \u2014 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." + }, + "v1_format_ref": { + "type": "array", + "minItems": 1, + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "description": "Authoritative v2 \u2192 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 \u2014 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` \u2014 see the 'Narrows \u2014 formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` \u2287 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 \u2014 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 \u2014 opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` \u2014 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 \u2192 registry glob \u2192 structural match \u2192 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 \u2192 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 \u2026) and the v1 wire fragments into per-publisher namespaces \u2014 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 \u2014 `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 \u22651 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/` \u2014 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." + }, + "format_schema": { + "title": "Platform Extension Reference", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://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 \u2014 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)** \u2014 `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 \u2014 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 \u2014 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 \u22645 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient \u2014 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** \u2014 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 \u00a76 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 \u22648 AND `$ref` count \u2264256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes \u2014 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 \u226410 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget \u2264250 ms (exceeded budget \u2192 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.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "format_kind": { + "const": "custom" + } + }, + "required": [ + "format_kind" + ] + }, + "then": { + "required": [ + "format_shape", + "format_schema" + ], + "anyOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + }, + "else": { + "not": { + "anyOf": [ + { + "required": [ + "format_shape" + ] + }, + { + "required": [ + "format_schema" + ] + } + ] + } + } + }, + { + "$comment": "canonical_formats_only:true and v1_format_ref are mutually exclusive \u2014 a declaration EITHER asserts no v1 projection OR links to a v1 named format, never both.", + "not": { + "allOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + } + }, + { + "$comment": "Canonical-format product declarations use format_option_id; capability_id belongs only to creative-agent build capabilities.", + "not": { + "required": [ + "capability_id" + ] + } + } + ], + "oneOf": [ + { + "title": "Image Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image" + }, + "params": { + "title": "Canonical Format: Image", + "description": "Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters \u2014 covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "image_main", + "asset_type": "image", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image canonical. Buyer ships an image asset (file or hosted URL) plus optional headline, body text, primary text (long-form caption), CTA (typically constrained to an enum via `cta_values`), and clickthrough URL. Products MAY override the default \u2014 make `headline` required, narrow `cta` to a value enum, or remove slots the surface doesn't consume." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required image width in pixels \u2014 use for fixed-size slots (e.g., a 300\u00d7250 IAB MREC). For multi-size flexible slots (publisher MREC slot that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250), use `sizes[]` instead; for responsive slots that adapt to viewport, use `min_width`/`max_width`/`min_height`/`max_height`. The three modes are mutually exclusive \u2014 set exactly one of `(width+height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required image height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. Buyer ships an asset matching one of the listed sizes; SDK validates `assets.image_main.{width,height}` against the list (any-match). Mirrors OpenRTB `banner.format[]` semantics \u2014 one declaration with N accepted sizes is cleaner than N format_options entries. Mutually exclusive with `(width, height)` and with `min/max_width` + `min/max_height` ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width in pixels for responsive slots that adapt within a range (e.g., 'any width from 300 to 970'). Use with `max_width` (and optionally `min_height`/`max_height`). Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width in pixels for responsive slots. Pair with `min_width`. See `min_width` for size-mode mutual exclusion." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height in pixels for responsive slots. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height in pixels for responsive slots. Pair with `min_height`." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside `width`/`height`, must agree. When used with `sizes[]` or responsive ranges, narrows accepted entries to those matching the aspect ratio." + }, + "max_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "svg" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the image and its trackers must be served over HTTPS." + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across all canonicals (`image`, `video_hosted`, `audio_hosted` \u2014 replaces the earlier per-canonical `image_source` / `video_source` / `audio_source` fields). `buyer_uploaded` (default): buyer ships a pre-rendered asset. `publisher_host_recorded`: publisher's host records the asset (audio-specific; podcast host-read pattern). `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE asset at sync_creatives or build_creative time (generative-DSP pattern). `seller_human_designed`: seller's design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class).\n\nNot every value is meaningful on every canonical \u2014 `publisher_host_recorded` is audio-specific; on `image` or `video_hosted` it has no defined behavior. Adopters MUST select a value appropriate to the canonical's asset type. The `slots` declaration is the binding contract for what the buyer ships; `asset_source` is informational and lets buyers understand the production model when picking products." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces 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`)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "HTML5 Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "html5" + }, + "params": { + "title": "Canonical Format: HTML5 Banner", + "description": "Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "html5_bundle", + "asset_type": "zip", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for html5 canonical. Buyer ships a zip bundle plus optional backup image (required when `backup_image_required: true`) and clickthrough URL. The zip's entry point is typically `index.html`; click handling uses the `clickTag` (or `clickTAG`) macro substituted by the seller at serve time." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required banner width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required banner height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot (publisher banner that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250). Mirrors OpenRTB `banner.format[]`. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive HTML5 banners that adapt within a range. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive HTML5 banners. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive HTML5 banners. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive HTML5 banners. Pair with `min_height`." + }, + "max_initial_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile." + }, + "max_polite_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes." + }, + "host_initiated_subload": { + "type": "boolean", + "description": "Whether the host page must initiate the polite-load phase. IAB-compliant banners require true." + }, + "max_animation_duration_ms": { + "type": "integer", + "minimum": 0, + "description": "Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds)." + }, + "max_cpu_load_percent": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Maximum CPU load percentage during render." + }, + "mraid_required": { + "type": "boolean", + "description": "Whether MRAID compatibility is required (mobile in-app)." + }, + "mraid_version": { + "type": "string", + "enum": [ + "2.0", + "3.0" + ], + "description": "Required MRAID version when mraid_required is true." + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether IAB Open Measurement SDK integration is required." + }, + "clicktag_macro": { + "type": "string", + "enum": [ + "clickTag", + "clickTAG" + ], + "description": "Name of the click-tag macro the bundle must use." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the zip for non-HTML5 environments." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum backup image file size in kilobytes." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Display Tag Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "display_tag" + }, + "params": { + "title": "Canonical Format: Display Tag", + "description": "Third-party-served display tag (JS, iframe, or 1\u00d71 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 \u2014 third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "tag_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + } + ], + "description": "Default slots for display_tag canonical. Buyer ships a URL pointing at the third-party-served creative (JS, iframe, or 1\u00d71 redirect) plus an optional backup image. Click and impression macros are substituted into the tag URL by the seller using `universal_macros`." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. The buyer's third-party tag must render at one of the listed sizes; the seller picks which size to request at impression time. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive third-party tags. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive third-party tags. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive third-party tags. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive third-party tags. Pair with `min_height`." + }, + "supported_tag_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "iframe", + "javascript", + "1x1_redirect" + ] + }, + "description": "Tag delivery mechanisms accepted." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the tag URL must be HTTPS." + }, + "max_redirect_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum redirect chain depth permitted." + }, + "max_response_time_ms": { + "type": "integer", + "minimum": 1, + "description": "Maximum tag-server response time in milliseconds." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the tag for environments that cannot render the third-party tag." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1 + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Image Carousel Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image_carousel" + }, + "params": { + "title": "Canonical Format: Image Carousel", + "description": "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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 multi-card carousels (Meta carousel, Pinterest pin collections, Snap collection ads) weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "cards", + "asset_type": "card", + "required": true, + "min": 2, + "max": 10 + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image_carousel. The `cards` slot's value in the manifest is an array of [card-asset](/schemas/core/assets/card-asset.json) objects; `min` / `max` constrain card count." + }, + "card_aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5')." + }, + "min_cards": { + "type": "integer", + "minimum": 2, + "description": "Minimum card count (typical: 2 or 3)." + }, + "max_cards": { + "type": "integer", + "description": "Maximum card count (typical: 6, 10, or 35 depending on platform)." + }, + "allowed_card_media_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "Asset types each card's `media` field may carry. Default: ['image']. Polymorphic carousels (Meta) allow ['image', 'video']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card's media payload, not the card-asset itself (which is always asset_type: \"card\")." + }, + "allowed_card_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "DEPRECATED \u2014 alias for `allowed_card_media_asset_types`. Kept for back-compat; prefer the new field name. Removed in 5.0." + }, + "card_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "card_video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum length of the carousel-level primary text." + }, + "card_headline_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card headline character limit. Governs the `headline` field on each card-asset in the `cards` slot." + }, + "card_description_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars)." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Video", + "description": "Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) \u2014 receivers fire impression and click pixels at delivery time.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "video_main", + "asset_type": "video", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "companion_banner", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_hosted canonical. Buyer ships a video asset (file or hosted URL); optional headline, primary text (long-form caption), CTA (typically constrained via `cta_values`), brand_name (typical for vertical short-form), companion_banner (typical for horizontal instream), and clickthrough URL. Products MAY override or extend the default \u2014 e.g., remove `companion_banner` for short-form vertical, narrow `cta` to a value enum, mark `landing_page_url` as required." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ], + "description": "Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed)." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio. Inferred from orientation if omitted." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: when both `duration_ms_exact` and `duration_ms_range` ship on the same product, `duration_ms_exact` takes precedence \u2014 buyers MUST validate against the exact value and ignore the range. The range is treated as advisory metadata in that case (e.g., for UI display showing the broader product family). SDKs SHOULD lint a warning when both fields ship; producers SHOULD pick one." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship (see `duration_ms_range` description)." + }, + "video_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "h264", + "h265", + "vp8", + "vp9", + "av1", + "prores" + ] + } + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aac", + "mp3", + "opus", + "pcm" + ] + } + }, + "containers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp4", + "webm", + "mov" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_file_size_mb": { + "type": "integer", + "minimum": 1 + }, + "frame_rates": { + "type": "array", + "items": { + "type": "number" + } + }, + "captions": { + "type": "string", + "enum": [ + "required", + "recommended", + "not_required" + ] + }, + "om_sdk_required": { + "type": "boolean" + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + } + }, + "companion_banner_widths": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + }, + "description": "Permitted companion banner widths (instream video)." + }, + "companion_banner_heights": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across canonicals. See `image.json#asset_source` for the full semantics. `publisher_host_recorded` is audio-specific and has no defined behavior on video \u2014 adopters MUST select a value appropriate to the canonical." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the video." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "VAST Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_vast" + }, + "params": { + "title": "Canonical Format: VAST Video", + "description": "VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "vast_tag", + "asset_type": "vast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_vast canonical. Buyer ships a VAST tag (URL or inline XML, VAST 2.x-4.x) plus an optional clickthrough URL (which falls back to the VAST `ClickThrough` element when omitted). Tracking events are inherent to VAST and don't require explicit slots." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ] + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$" + }, + "vast_version": { + "type": "string", + "enum": [ + "2.0", + "3.0", + "4.0", + "4.1", + "4.2" + ], + "description": "Required VAST version." + }, + "vpaid_enabled": { + "type": "boolean", + "description": "Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads." + }, + "vpaid_version": { + "type": "string", + "enum": [ + "1.0", + "2.0" + ] + }, + "simid_supported": { + "type": "boolean", + "description": "Whether IAB SIMID interactive video extensions are supported." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "linear_required": { + "type": "boolean", + "description": "Whether the VAST creative must be linear (non-skippable in-stream)." + }, + "skippable_after_ms": { + "type": "integer", + "minimum": 0, + "description": "When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll)." + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum VAST wrapper redirect depth permitted." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Audio", + "description": "Direct audio creative \u2014 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) \u2014 there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "audio_main", + "asset_type": "audio", + "required": true + }, + { + "asset_group_id": "companion_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for buyer-uploaded audio. Host-read products override with a `script` (asset_type: text) or `creative_brief` (asset_type: brief) slot in place of `audio_main`, plus `asset_source: 'publisher_host_recorded'` and `buyer_asset_acceptance: 'rejected'`. TTS-from-script products override similarly with `asset_source: 'seller_pre_rendered_from_brief'`." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship on the same product. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp3", + "aac", + "wav", + "opus", + "flac" + ] + } + }, + "audio_sample_rates": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "audio_channels": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mono", + "stereo" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "loudness_lufs": { + "type": "number", + "description": "Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values." + }, + "loudness_tolerance_db": { + "type": "number", + "minimum": 0, + "description": "Permitted deviation from loudness_lufs in dB." + }, + "true_peak_dbfs": { + "type": "number", + "description": "Maximum true-peak level in dBFS (typical: -2)." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered audio bytes come from. Single shared enum across canonicals (see `image.json#asset_source` for the full semantics). `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. This value is audio-specific." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `asset_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`)." + }, + "companion_image_required": { + "type": "boolean" + }, + "companion_image_aspect_ratio": { + "type": "string" + }, + "companion_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "DAAST Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_daast" + }, + "params": { + "title": "Canonical Format: DAAST Audio", + "description": "DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "daast_tag", + "asset_type": "daast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for audio_daast canonical. Buyer ships a DAAST tag (URL or inline XML, 1.0 or 1.1) plus an optional clickthrough URL. Tracking events are inherent to DAAST and don't require explicit slots." + }, + "daast_version": { + "type": "string", + "enum": [ + "1.0", + "1.1" + ] + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "linear_required": { + "type": "boolean" + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0 + }, + "ssl_required": { + "type": "boolean" + }, + "companion_image_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Sponsored Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "sponsored_placement" + }, + "params": { + "title": "Canonical Format: Sponsored Placement (retail-media catalog-driven)", + "description": "Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset \u2014 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** \u2014 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 \u2014 buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot \u2014 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) \u2014 use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Single-image or single-video creative \u2014 use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing \u2014 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).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical covers 4 meaningfully different retail-media adapter contracts (Amazon SP, Criteo SP / CitrusAd SP, Pinterest Collection, generative-per-SKU). Adopter contracts vary; buyers MUST validate per-adapter behavior before routing budget. Promotion to non-experimental gated on the #4592 adapter-contract docs work." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 retail-media catalog placements weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural, not a registry-coverage gap." + }, + "slots": { + "default": [ + { + "asset_group_id": "source_catalog", + "required": true, + "asset_type": "catalog" + }, + { + "asset_group_id": "hero_asset", + "required": false, + "asset_type": "image" + }, + { + "asset_group_id": "landing_page_url", + "required": false, + "asset_type": "url" + } + ] + }, + "supported_catalog_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "product", + "store", + "offering", + "hotel", + "flight", + "vehicle", + "real_estate", + "education", + "destination", + "app", + "job", + "inventory" + ] + }, + "description": "Catalog types this product accepts." + }, + "min_items": { + "type": "integer", + "minimum": 1, + "description": "Minimum catalog item count buyer must supply." + }, + "max_items": { + "type": "integer", + "description": "Maximum items considered for placement." + }, + "fanout_mode": { + "type": "string", + "enum": [ + "per_item", + "multi_item_in_creative", + "single_item" + ], + "description": "How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item." + }, + "required_catalog_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])." + }, + "supported_id_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "asin", + "sku", + "gtin", + "offering_id", + "store_id", + "hotel_id", + "flight_id", + "vehicle_id", + "listing_id", + "program_id", + "destination_id", + "app_id", + "job_id" + ] + }, + "description": "Catalog identifier types the placement renders against." + }, + "hero_asset_supported": { + "type": "boolean", + "description": "Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern)." + }, + "item_production_model": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "How each per-item creative is produced. Covers the same production-source axis as `asset_source` on `image` / `video_hosted` / `audio_hosted` but with a 4-value subset \u2014 drops `publisher_host_recorded` because it's audio-specific and doesn't apply to retail-media catalog placements. SDK codegen MAY share a base enum and narrow per-canonical, or emit two distinct enums; either way the wire values overlap exactly for the 4 retained values. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer's catalog already contains rendered assets per item; the seller composes the placement using those assets. (\"Uploaded\" reads slightly off for catalog-keyed items where the buyer didn't actively upload bytes \u2014 the catalog ingestion already supplied them \u2014 but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller's design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief \u00d7 N catalog items \u2192 N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Native In-Feed Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "native_in_feed" + }, + "params": { + "title": "Canonical Format: Native In-Feed", + "description": "IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types \u2014 `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 \u2014 visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 \u2014 app/product rating), `price` (Data Asset type 6 \u2014 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 \u2014 type 4, downloads \u2014 type 5, saleprice \u2014 type 7, phone_number \u2014 type 8, address \u2014 type 9, desc2 \u2014 type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel \u2014 feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative \u2014 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) \u2014 use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Multi-card carousel \u2014 use `image_carousel`.\n- Video-first native units where the asset is a hosted video file \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": false, + "description": "Stable at 3.1 GA. Shape mirrors IAB OpenRTB Native 1.2 \u2014 the renderer contract is well-established across in-feed native and content-recommendation adopters." + }, + "v1_translatable": { + "default": true, + "description": "Translates to v1 named native formats (e.g., `native_standard`, `native_content`) via the projection registry. Sellers with existing v1 named native formats SHOULD point `v1_format_ref[]` at them." + }, + "slots": { + "default": [ + { + "asset_group_id": "title", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "main_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "icon", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "advertiser_name", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "sponsored_label", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "display_url", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "rating", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "price", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "impression_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "viewability_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "click_tracker", + "asset_type": "pixel_tracker", + "required": false + } + ], + "description": "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)." + }, + "title_max_chars": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the body_text slot. IAB native typical: 90 (mainline) to 140 (extended)." + }, + "cta_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the cta slot. Typical: 15\u201325." + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD']). When set, narrows the cta slot to a closed enum." + }, + "main_image_sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "Accepted (width, height) pairs for the main_image slot. Common IAB native sizes: 1200\u00d7627 (1.91:1), 1080\u00d71080 (1:1), 1080\u00d71350 (4:5)." + }, + "icon_size": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false, + "description": "Required (width, height) for the icon slot when present (typical: 80\u00d780 or 100\u00d7100)." + }, + "max_image_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes for main_image and icon." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether trackers, landing pages, and image URLs must be served over HTTPS." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered native assets come from. `publisher_host_recorded` is omitted (audio-specific and not meaningful for native). Other values mirror the shared production-source axis used on `image` / `video_hosted`. `buyer_uploaded` (default): buyer ships pre-rendered title/image/body. `seller_pre_rendered_from_brief`: buyer ships a brief, seller renders the native bundle. `agent_synthesized`: AI synthesis pipeline produces title + image + body from a brief; pair with `synthesis_nondeterministic: true` for generative pipelines that can't guarantee in-spec output." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded native assets. When `rejected`, the buyer cannot ship pre-rendered title/image/body \u2014 they must use `build_creative` (or `sync_creatives` with brief inputs) so the seller produces the native bundle from a brief." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Responsive Creative Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "responsive_creative" + }, + "params": { + "title": "Canonical Format: Responsive Creative", + "description": "Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** \u2014 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.).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: composition is algorithmic (the surface picks combinations and reports per-asset breakdowns), and there's no clean v1-translatable equivalent. Buyers ship asset pools rather than rendered creatives; the surface's per-impression composition cannot be predicted by `validate_input`. Adopters SHOULD validate behavior per surface (Google PMax vs Meta Advantage+ creative differ meaningfully)." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 algorithmic asset-pool composition (Google PMax / Meta Advantage+ creative) wasn't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "headlines", + "asset_type": "text", + "required": true, + "min": 3, + "max": 15 + }, + { + "asset_group_id": "long_headlines", + "asset_type": "text", + "required": false, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "descriptions", + "asset_type": "text", + "required": true, + "min": 2, + "max": 5 + }, + { + "asset_group_id": "images_landscape", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_square", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_vertical", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "video", + "asset_type": "video", + "required": false, + "min": 0, + "max": 5 + }, + { + "asset_group_id": "logo", + "asset_type": "image", + "required": true, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true, + "min": 1, + "max": 1 + } + ] + }, + "headlines_min": { + "type": "integer", + "minimum": 0 + }, + "headlines_max": { + "type": "integer", + "minimum": 0 + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "long_headlines_min": { + "type": "integer", + "minimum": 0 + }, + "long_headlines_max": { + "type": "integer", + "minimum": 0 + }, + "long_headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "descriptions_min": { + "type": "integer", + "minimum": 0 + }, + "descriptions_max": { + "type": "integer", + "minimum": 0 + }, + "description_max_chars": { + "type": "integer", + "minimum": 1 + }, + "images_landscape_min": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_max": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_aspect_ratio": { + "type": "string" + }, + "images_square_min": { + "type": "integer", + "minimum": 0 + }, + "images_square_max": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_min": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_max": { + "type": "integer", + "minimum": 0 + }, + "videos_min": { + "type": "integer", + "minimum": 0 + }, + "videos_max": { + "type": "integer", + "minimum": 0 + }, + "video_min_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "logo_min": { + "type": "integer", + "minimum": 0 + }, + "logo_max": { + "type": "integer", + "minimum": 0 + }, + "logo_aspect_ratios": { + "type": "array", + "items": { + "type": "string" + } + }, + "business_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "asset_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "supports_catalog_input": { + "type": "boolean", + "description": "Whether the product can additionally consume a catalog reference (e.g., PMax with product feed)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Agent Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "agent_placement" + }, + "params": { + "title": "Canonical Format: Agent Placement (AI-surface sponsored placement)", + "description": "**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 set `runtime_status: 'preview'` or `'declared_only'` on the declaration; 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** \u2014 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 \u2014 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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical's tracking model (mention-level impression + attribution, postback shape, cross-surface dedup) is intentionally underspecified for 3.1. Adopters claiming `agent_placement` ship private tracking integrations; buyer agents MUST treat attribution as adapter-defined until the 3.2 tracking-macro spec lands. Promotion to non-experimental gated on the 3.2 tracking-contract spec." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 AI-surface sponsored mentions weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "offering_ref", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "agent_placement has minimal buyer-shipped slots \u2014 the surface composes the rendered output from brand context (resolved via the manifest's top-level `brand` BrandRef) plus optional offering_ref and landing_page_url assets. None of these assets are rendered verbatim by the buyer; the agent chooses how to use them." + }, + "output_modality": { + "type": "string", + "enum": [ + "text", + "audio", + "card" + ], + "description": "How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text." + }, + "max_mention_length_chars": { + "type": "integer", + "minimum": 1, + "description": "For text output: maximum length of the surface-composed mention text." + }, + "max_mention_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "For audio output: maximum duration of the spoken mention in milliseconds." + }, + "supports_offering_reference": { + "type": "boolean", + "description": "Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context." + }, + "supports_landing_page_url": { + "type": "boolean", + "description": "Whether the surface attaches a landing page URL to the mention (citation, learn-more link)." + }, + "tone_constraints": { + "type": "array", + "items": { + "type": "string" + }, + "description": "**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement \u2014 adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance." + }, + "disclosure_required": { + "type": "boolean", + "description": "Whether the surface must include an explicit sponsorship disclosure label." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Custom Format Declaration", + "description": "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 \u2014 kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.", + "properties": { + "format_kind": { + "type": "string", + "const": "custom" + }, + "params": { + "type": "object", + "additionalProperties": true, + "description": "Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`." + } + }, + "required": [ + "format_kind", + "params" + ] + } + ], + "examples": [ + { + "description": "Meta Reels \u2014 narrows video_hosted (vertical orientation)", + "data": { + "format_kind": "video_hosted", + "params": { + "orientation": "vertical", + "aspect_ratio": "9:16", + "duration_ms_range": [ + 3000, + 90000 + ], + "min_width": 1080, + "min_height": 1920, + "max_file_size_mb": 200, + "video_codecs": [ + "h264" + ], + "audio_codecs": [ + "aac" + ], + "headline_max_chars": 25, + "primary_text_max_chars": 72, + "captions": "recommended", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "DOWNLOAD", + "SIGN_UP" + ], + "composition_model": "deterministic", + "platform_extensions": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + } + }, + { + "description": "IAB Medium Rectangle (300x250) \u2014 narrows image", + "data": { + "format_kind": "image", + "params": { + "width": 300, + "height": 250, + "max_file_size_kb": 200, + "image_formats": [ + "jpg", + "png", + "gif" + ], + "ssl_required": true, + "composition_model": "deterministic", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "GET_OFFER" + ] + } + } + }, + { + "description": "Podcast 30s host-read \u2014 narrows audio_hosted with a `script` slot the seller's host reads verbatim. No separate `inputs` map; the script lives in the manifest's `assets` like any other text asset.", + "data": { + "format_kind": "audio_hosted", + "params": { + "duration_ms_exact": 30000, + "audio_codecs": [ + "mp3", + "aac" + ], + "audio_sample_rates": [ + 44100, + 48000 + ], + "audio_channels": [ + "stereo" + ], + "loudness_lufs": -16, + "asset_source": "publisher_host_recorded", + "buyer_asset_acceptance": "rejected", + "composition_model": "deterministic", + "slots": [ + { + "asset_group_id": "script", + "required": true, + "asset_type": "text", + "max_chars": 800 + }, + { + "asset_group_id": "offering_ref", + "required": false, + "asset_type": "text" + } + ], + "production_window_business_days": 7 + } + } + }, + { + "description": "NYTimes Homepage Takeover \u2014 custom format_kind, classified against the multi_placement_takeover format_shape, with format_schema pointing at NYTimes's hosted schema. Buyer agents fetch the schema by uri@digest (cached, immutable) and validate the manifest structurally. `canonical_formats_only: true` is required for custom declarations \u2014 no v1 named format can express the multi-placement shape.", + "data": { + "format_kind": "custom", + "canonical_formats_only": true, + "format_shape": "multi_placement_takeover", + "format_schema": { + "uri": "https://nytimes.example/schemas/formats/homepage_takeover_v3", + "digest": "sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3" + }, + "format_option_id": "nytimes_homepage_takeover_premium", + "display_name": "Homepage Takeover \u2014 Premium Sponsorship", + "applies_to_channels": [ + "display", + "olv" + ], + "params": { + "components": [ + { + "placement_type": "homepage_skin", + "required": true + }, + { + "placement_type": "preroll_video", + "required": true + }, + { + "placement_type": "sponsorship_lockup", + "required": true + } + ], + "exclusivity_window_hours": 24, + "ssl_required": true + } + } + } + ] + } + }, + "placements": { + "type": "array", + "description": "Optional array of specific public placements within this product. Placement IDs are scoped by publisher domain. Product placements declare `kind` to distinguish publisher-referenced placements (`publisher_ref`) from seller-defined inline placements (`seller_inline`). Publisher-referenced placements carry `publisher_domain` plus `placement_id` and may omit `name` because buyers resolve the name from the publisher's adagents.json placement declarations. Seller-inline placements carry buyer-facing `name` directly; when `publisher_domain` is omitted, buyers MAY interpret the placement ID relative to the seller agent's own publisher domain only during the legacy single-publisher transition. Community-maintained fallback files are resolver/source metadata, not a distinct placement kind. Each placement MUST declare `mode: 'targetable'` (buyer may select the placement by PlacementRef, for example in creative assignments) or `mode: 'included'` (part of the public product composition but not buyer-selectable). Placement-level format declarations narrow the product-level creative contract and MUST NOT broaden it. Seller-private delivery objects, source/origin details, and ad-server mappings MUST NOT be exposed here.", + "items": { + "title": "Placement", + "description": "Represents a specific public ad placement within a product's inventory. Placement IDs are scoped by publisher domain, matching placement definitions in that publisher's adagents.json. `kind` is the structural discriminator: `publisher_ref` means this product placement is a reference to `{publisher_domain, placement_id}`; `seller_inline` means the seller is defining public buyer-facing placement metadata inline. The schema accepts either `name` or `publisher_domain` because publisher-referenced placements can omit `name` only when the publisher declaration supplies it; seller-inline placements carry `name` directly. Whether a reference was resolved from publisher-hosted adagents.json or a community-maintained fallback is resolver metadata, not placement structure. Buyers reference placements in creative assignments with structured PlacementRef objects (`publisher_domain` + `placement_id`) when a product spans multiple publishers or the namespace is otherwise ambiguous. Reusing a registered placement preserves the registry's semantic identity; product-level placement objects may narrow format_ids/format_options or add operational detail, but SHOULD NOT redefine the placement's meaning incompatibly.", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "publisher_ref", + "seller_inline" + ], + "description": "Placement structure discriminator. `publisher_ref` identifies a placement by `{publisher_domain, placement_id}` and resolves public metadata from the named publisher's adagents.json placement declarations; `seller_inline` identifies buyer-facing placement metadata defined inline by the sales agent (still in the named publisher namespace when `publisher_domain` is present, or the seller's own namespace in legacy single-publisher contexts)." + }, + "placement_id": { + "type": "string", + "description": "Placement identifier in the publisher namespace. When `publisher_domain` is present, this matches a placement ID in that publisher's adagents.json catalog or a seller-defined inline placement in that publisher namespace. Buyers use this with `publisher_domain` in `creative_assignments[].placement_refs`; legacy `creative_assignments[].placement_ids` strings are only unambiguous in single-publisher contexts." + }, + "publisher_domain": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "description": "Publisher domain whose adagents.json placement declarations define this placement. Required for `kind: \"publisher_ref\"`. Omitted only for `kind: \"seller_inline\"` in legacy single-publisher seller contexts where the seller agent's own publisher domain is the namespace." + }, + "name": { + "type": "string", + "description": "Human-readable name for the placement (e.g., 'Homepage Banner', 'Article Sidebar'). Required for `kind: \"seller_inline\"`. May be omitted for publisher-referenced placements because buyers resolve the name from the publisher declaration identified by `{publisher_domain, placement_id}`." + }, + "description": { + "type": "string", + "description": "Detailed description of where and how the placement appears" + }, + "mode": { + "type": "string", + "enum": [ + "targetable", + "included" + ], + "description": "Required product-level relationship to this placement. `targetable` means the buyer may reference this placement_id when assigning creatives or otherwise selecting placements within the product. `included` means the placement is part of the product's public delivery composition but the buyer cannot cherry-pick it by placement_id. During the migration window ending 2026-11-25, buyers MAY tolerate legacy products that omit `mode` and treat them as targetable; after that date buyers SHOULD fail closed. Seller-private delivery objects MUST NOT be exposed here; keep those mappings in seller-internal systems." + }, + "tags": { + "type": "array", + "description": "Optional tags for grouping placements within a product (e.g., 'homepage', 'native', 'premium'). When the placement_id comes from the publisher registry, these should align with the registry tags unless the product is narrowing scope.", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "format_ids": { + "type": "array", + "description": "Format IDs supported by this specific placement. Can include: (1) concrete format_ids (fixed dimensions), (2) template format_ids without parameters (accepts any dimensions/duration), or (3) parameterized format_ids (specific dimension/duration constraints). When present on a product placement, this field narrows the product-level `format_ids` contract for this placement and MUST NOT introduce formats the product does not accept.", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "minItems": 1 + }, + "format_options": { + "type": "array", + "description": "3.1+ canonical format-option declarations supported by this specific product placement. When present, this field narrows the product-level `format_options` contract for this placement and MUST NOT introduce formats the product does not accept. Buyers compute the effective accepted formats for a placement as the intersection of product-level and placement-level declarations; placements without a format declaration inherit the product-level formats.", + "items": { + "title": "Product Format Declaration", + "description": "Inline format declaration on a product. The `format_kind` discriminator names which canonical format the product narrows; `params` carries the canonical's parameter schema (slots, dimensions, durations, codecs, character limits, platform_extensions, 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 \u2014 lets a multi-channel product carry distinct format_options per channel). Discriminated-union shape generates clean tagged unions in TypeScript and Pydantic codegen. Replaces v1's named-format pattern (where products referenced a separately-defined format file via compound `format_id`). v1 named formats remain supported through the deprecation cycle; v2 product-bound declarations are opt-in.\n\n**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 \u2014 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**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.", + "type": "object", + "required": [ + "format_kind", + "params" + ], + "discriminator": { + "propertyName": "format_kind" + }, + "properties": { + "format_option_id": { + "type": "string", + "description": "Stable identifier for this format declaration within its namespace. REQUIRED when the parent product's `format_options` contains multiple declarations sharing the same `format_kind` (so buyers can disambiguate which option a manifest targets via `manifest.format_option_ref`). SHOULD be set on EVERY `format_options[]` entry \u2014 not just when structurally required to break a `format_kind` collision \u2014 so V2-mental-model buyers can use the V2 authoring path (`PackageRequest.format_option_refs[]`, `creative-manifest.format_option_ref`) against the product. Publisher-catalog-backed options pair this with `publisher_domain`; product-local options omit `publisher_domain` and are selected by `format_option_id` within the target product. A product that ships without selectable `format_option_id` values on its `format_options[]` entries is structurally 3.1-conformant but is not V2-authorable: buyers fall back to v1 `format_ids[]` and lose the stable naming the V2 path was designed to provide. Sellers MUST reject V2 authoring against such products with `UNSUPPORTED_FEATURE` and `error.details.reason` set to `format_option_refs_not_published` per `package-request.json`. Format-internal (not a URI). Examples: 'display_image_300x250', 'responsive_search', 'daily_pulse_homepage_image'." + }, + "publisher_domain": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "description": "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." + }, + "display_name": { + "type": "string", + "description": "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 \u2014 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." + }, + "applies_to_channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "uniqueItems": true, + "description": "Optional subset of the parent product's `channels` to which this declaration applies. When omitted, the declaration applies to ALL channels declared on the product. Lets a multi-channel product (e.g., `channels: ['display', 'video']`) carry distinct format_options per channel \u2014 `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`." + }, + "seller_preference": { + "type": "string", + "enum": [ + "preferred", + "accepted", + "discouraged" + ], + "description": "Optional soft routing hint *within* a product's accepted set of formats \u2014 NOT an enforcement axis. `preferred` \u2014 seller actively recommends this format (often because of measurement, viewability, or render-quality differences); `accepted` \u2014 supported on equal footing with other format_options (default when omitted); `discouraged` \u2014 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 \u2014 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." + }, + "canonical_formats_only": { + "type": "boolean", + "default": false, + "description": "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 \u2014 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` \u2014 explicit v2-only is more useful than silent absence." + }, + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, THIS seller's specific product declaration may not work as declared \u2014 even if the underlying canonical is stable. Use for beta runtime paths, forward-looking catalog entries the runtime doesn't yet honor, or experimental products where the seller wants buyer-side caution. Buyers reading `experimental: true` on a product declaration SHOULD prefer the legacy named-format path when a fallback exists for the same product (via `format_ids` on the parent product or via this declaration's `v1_format_ref`) and SHOULD validate via `validate_input` or a sandbox before routing production budget.\n\nIndependent of the canonical's own `experimental` flag \u2014 a stable canonical (e.g., `image`, `video_hosted`) can carry an experimental product declaration when the seller is shipping a new runtime path that isn't fully wired yet. Conversely, an experimental canonical (`sponsored_placement`, `responsive_creative`, `agent_placement`) MAY carry non-experimental product declarations where the seller's adopter contract is well-tested. Buyer SDKs SHOULD filter products with `experimental: true` from default views and offer an opt-in flag to surface them.\n\nReplaces the earlier `runtime_status` enum (`stable | preview | declared_only`) \u2014 same semantic ('use with caution') without the cognitive overhead of two stability axes." + }, + "format_shape": { + "type": "string", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. Recognized global pattern this custom shape is an instance of, drawn from the [format-shape vocabulary registry](/schemas/core/format-shape-vocabulary.json) (`multi_placement_takeover`, `roadblock`, `branded_content`, `cross_screen_sponsorship`, `sponsorship_lockup`, `newsletter_sponsorship`, `ar_lens`, `playable`, `live_event_sponsorship`, \u2026). Non-canonical values valid (validators MAY soft-warn) \u2014 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." + }, + "v1_format_ref": { + "type": "array", + "minItems": 1, + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "description": "Authoritative v2 \u2192 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 \u2014 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` \u2014 see the 'Narrows \u2014 formal definition' section in canonical-formats.mdx. SDKs comparing dual-emitted shapes (`Product.format_ids[]` \u2287 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 \u2014 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 \u2014 opt-in, requires catalog access; sellers asserting refs is the source of truth.\n\nMutually exclusive with `canonical_formats_only: true` \u2014 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 \u2192 registry glob \u2192 structural match \u2192 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 \u2192 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 \u2026) and the v1 wire fragments into per-publisher namespaces \u2014 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 \u2014 `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 \u22651 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/` \u2014 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." + }, + "format_schema": { + "title": "Platform Extension Reference", + "description": "REQUIRED when `format_kind: \"custom\"`; otherwise MUST be absent. URI+digest reference to a fetchable schema describing this custom shape's actual `params` and `slots`. Same hosting model as `platform_extensions`: open-ecosystem publishers host the artifact at the canonical URI on their subdomain; closed-platform / walled-garden shapes resolve through the AAO mirror at `https://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 \u2014 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)** \u2014 `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 \u2014 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 \u2014 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 \u22645 seconds. Timeout SHOULD be treated identically to an HTTP 5xx response (transient \u2014 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** \u2014 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 \u00a76 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 \u22648 AND `$ref` count \u2264256 across the resolved tree (depth 8 with breadth 100 per level is 10^16 nodes \u2014 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 \u226410 000, `pattern` regexes evaluated with a non-backtracking engine (re2) OR under a per-pattern timeout, per-manifest validation budget \u2264250 ms (exceeded budget \u2192 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.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "format_kind": { + "const": "custom" + } + }, + "required": [ + "format_kind" + ] + }, + "then": { + "required": [ + "format_shape", + "format_schema" + ], + "anyOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + }, + "else": { + "not": { + "anyOf": [ + { + "required": [ + "format_shape" + ] + }, + { + "required": [ + "format_schema" + ] + } + ] + } + } + }, + { + "$comment": "canonical_formats_only:true and v1_format_ref are mutually exclusive \u2014 a declaration EITHER asserts no v1 projection OR links to a v1 named format, never both.", + "not": { + "allOf": [ + { + "properties": { + "canonical_formats_only": { + "const": true + } + }, + "required": [ + "canonical_formats_only" + ] + }, + { + "required": [ + "v1_format_ref" + ] + } + ] + } + }, + { + "$comment": "Canonical-format product declarations use format_option_id; capability_id belongs only to creative-agent build capabilities.", + "not": { + "required": [ + "capability_id" + ] + } + } + ], + "oneOf": [ + { + "title": "Image Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image" + }, + "params": { + "title": "Canonical Format: Image", + "description": "Static image creative format. Slots: `image_main` (image asset, file or hosted URL), optional `headline` (text), `body_text` (text), `cta` (text/enum), `landing_page_url` (url). Tracking model: impression pixel + click URL via universal_macros, with optional viewability pixel. Distinct from `html5` (interactive bundles) and `display_tag` (third-party served). AR/dimensions narrow to specific sizes via product parameters \u2014 covers IAB display sizes (300x250, 728x90, 970x250, etc.) without a separate iab_size enum.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes (e.g., `width` + `sizes`) is rejected at schema layer; same rule on `html5` and `display_tag` canonicals.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "image_main", + "asset_type": "image", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image canonical. Buyer ships an image asset (file or hosted URL) plus optional headline, body text, primary text (long-form caption), CTA (typically constrained to an enum via `cta_values`), and clickthrough URL. Products MAY override the default \u2014 make `headline` required, narrow `cta` to a value enum, or remove slots the surface doesn't consume." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required image width in pixels \u2014 use for fixed-size slots (e.g., a 300\u00d7250 IAB MREC). For multi-size flexible slots (publisher MREC slot that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250), use `sizes[]` instead; for responsive slots that adapt to viewport, use `min_width`/`max_width`/`min_height`/`max_height`. The three modes are mutually exclusive \u2014 set exactly one of `(width+height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required image height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. Buyer ships an asset matching one of the listed sizes; SDK validates `assets.image_main.{width,height}` against the list (any-match). Mirrors OpenRTB `banner.format[]` semantics \u2014 one declaration with N accepted sizes is cleaner than N format_options entries. Mutually exclusive with `(width, height)` and with `min/max_width` + `min/max_height` ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width in pixels for responsive slots that adapt within a range (e.g., 'any width from 300 to 970'). Use with `max_width` (and optionally `min_height`/`max_height`). Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width in pixels for responsive slots. Pair with `min_width`. See `min_width` for size-mode mutual exclusion." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height in pixels for responsive slots. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height in pixels for responsive slots. Pair with `min_height`." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Optional aspect ratio constraint (e.g., '1.91:1', '1:1'). When provided alongside `width`/`height`, must agree. When used with `sizes[]` or responsive ranges, narrows accepted entries to those matching the aspect ratio." + }, + "max_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "svg" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the image and its trackers must be served over HTTPS." + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW'])." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across all canonicals (`image`, `video_hosted`, `audio_hosted` \u2014 replaces the earlier per-canonical `image_source` / `video_source` / `audio_source` fields). `buyer_uploaded` (default): buyer ships a pre-rendered asset. `publisher_host_recorded`: publisher's host records the asset (audio-specific; podcast host-read pattern). `seller_pre_rendered_from_brief`: buyer ships a brief plus structured copy; seller renders ONE asset at sync_creatives or build_creative time (generative-DSP pattern). `seller_human_designed`: seller's design team renders manually from a brief. `agent_synthesized`: AI synthesis pipeline; pair with `synthesis_nondeterministic: true` when the platform cannot guarantee in-spec output (Veo/Sora/Imagen-class).\n\nNot every value is meaningful on every canonical \u2014 `publisher_host_recorded` is audio-specific; on `image` or `video_hosted` it has no defined behavior. Adopters MUST select a value appropriate to the canonical's asset type. The `slots` declaration is the binding contract for what the buyer ships; `asset_source` is informational and lets buyers understand the production model when picking products." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded assets. When `rejected`, the buyer cannot ship pre-rendered bytes directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces 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`)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "HTML5 Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "html5" + }, + "params": { + "title": "Canonical Format: HTML5 Banner", + "description": "Interactive HTML5 banner delivered as a zip archive. Slot: `html5_bundle` (zip asset). Tracking model: MRAID + IAB Open Measurement (OM-SDK) + click-tag macro substitution + backup image fallback. Receivers unpack the zip, validate internal structure, and serve from CDN. Distinct from `image` (static, non-interactive) and `display_tag` (third-party served). The zip's entry point is typically `index.html`; click handling uses `clickTag` (or `clickTAG`) macro substitution.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "html5_bundle", + "asset_type": "zip", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for html5 canonical. Buyer ships a zip bundle plus optional backup image (required when `backup_image_required: true`) and clickthrough URL. The zip's entry point is typically `index.html`; click handling uses the `clickTag` (or `clickTAG`) macro substituted by the seller at serve time." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required banner width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required banner height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot (publisher banner that accepts 300\u00d7250 OR 728\u00d790 OR 970\u00d7250). Mirrors OpenRTB `banner.format[]`. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive HTML5 banners that adapt within a range. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive HTML5 banners. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive HTML5 banners. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive HTML5 banners. Pair with `min_height`." + }, + "max_initial_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum initial-load file size (zip + above-the-fold assets) in kilobytes. IAB display standards: 200 KB for fixed sizes, 100 KB for mobile." + }, + "max_polite_load_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum polite-load file size after host-initiated subload, in kilobytes. IAB display standards: 500 KB for fixed sizes." + }, + "host_initiated_subload": { + "type": "boolean", + "description": "Whether the host page must initiate the polite-load phase. IAB-compliant banners require true." + }, + "max_animation_duration_ms": { + "type": "integer", + "minimum": 0, + "description": "Maximum total animation duration in milliseconds. IAB standard: 30000 (30 seconds)." + }, + "max_cpu_load_percent": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Maximum CPU load percentage during render." + }, + "mraid_required": { + "type": "boolean", + "description": "Whether MRAID compatibility is required (mobile in-app)." + }, + "mraid_version": { + "type": "string", + "enum": [ + "2.0", + "3.0" + ], + "description": "Required MRAID version when mraid_required is true." + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether IAB Open Measurement SDK integration is required." + }, + "clicktag_macro": { + "type": "string", + "enum": [ + "clickTag", + "clickTAG" + ], + "description": "Name of the click-tag macro the bundle must use." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the zip for non-HTML5 environments." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum backup image file size in kilobytes." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Display Tag Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "display_tag" + }, + "params": { + "title": "Canonical Format: Display Tag", + "description": "Third-party-served display tag (JS, iframe, or 1\u00d71 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 \u2014 third party serves and measures. Click tracking via redirect URL substitution using universal_macros. Distinct from `image` (static asset hosted by seller) and `html5` (zip bundle hosted by seller).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + }, + { + "title": "Size-mode mutex", + "description": "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 \u2014 accepts any dimensions). Combining modes is rejected at schema layer.", + "oneOf": [ + { + "title": "fixed", + "required": [ + "width", + "height" + ], + "not": { + "anyOf": [ + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "multi-size", + "required": [ + "sizes" + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + }, + { + "title": "responsive", + "anyOf": [ + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + } + ] + } + }, + { + "title": "none", + "not": { + "anyOf": [ + { + "required": [ + "width" + ] + }, + { + "required": [ + "height" + ] + }, + { + "required": [ + "sizes" + ] + }, + { + "required": [ + "min_width" + ] + }, + { + "required": [ + "max_width" + ] + }, + { + "required": [ + "min_height" + ] + }, + { + "required": [ + "max_height" + ] + } + ] + } + } + ] + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "tag_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "backup_image", + "asset_type": "image", + "required": false + } + ], + "description": "Default slots for display_tag canonical. Buyer ships a URL pointing at the third-party-served creative (JS, iframe, or 1\u00d71 redirect) plus an optional backup image. Click and impression macros are substituted into the tag URL by the seller using `universal_macros`." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering width in pixels \u2014 use for fixed-size slots. For multi-size flexible slots use `sizes[]`; for responsive use `min_width`/`max_width`/`min_height`/`max_height`. Exactly one of `(width, height)`, `sizes[]`, or `min/max_width` + `min/max_height` ranges MUST be set." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Required tag rendering height in pixels. See `width` for size-mode mutual exclusion." + }, + "sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "List of accepted (width, height) pairs for a multi-size flexible slot. The buyer's third-party tag must render at one of the listed sizes; the seller picks which size to request at impression time. Mutually exclusive with `(width, height)` and with responsive ranges." + }, + "min_width": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted width for responsive third-party tags. Pair with `max_width`. Mutually exclusive with `(width, height)` and `sizes[]`." + }, + "max_width": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted width for responsive third-party tags. Pair with `min_width`." + }, + "min_height": { + "type": "integer", + "minimum": 1, + "description": "Minimum accepted height for responsive third-party tags. Pair with `max_height`." + }, + "max_height": { + "type": "integer", + "minimum": 1, + "description": "Maximum accepted height for responsive third-party tags. Pair with `min_height`." + }, + "supported_tag_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "iframe", + "javascript", + "1x1_redirect" + ] + }, + "description": "Tag delivery mechanisms accepted." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether the tag URL must be HTTPS." + }, + "max_redirect_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum redirect chain depth permitted." + }, + "max_response_time_ms": { + "type": "integer", + "minimum": 1, + "description": "Maximum tag-server response time in milliseconds." + }, + "backup_image_required": { + "type": "boolean", + "description": "Whether a backup image must accompany the tag for environments that cannot render the third-party tag." + }, + "backup_image_max_size_kb": { + "type": "integer", + "minimum": 1 + }, + "om_sdk_required": { + "type": "boolean", + "description": "Whether the buyer's tag must integrate IAB Open Measurement SDK for viewability." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Image Carousel Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "image_carousel" + }, + "params": { + "title": "Canonical Format: Image Carousel", + "description": "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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 multi-card carousels (Meta carousel, Pinterest pin collections, Snap collection ads) weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "cards", + "asset_type": "card", + "required": true, + "min": 2, + "max": 10 + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for image_carousel. The `cards` slot's value in the manifest is an array of [card-asset](/schemas/core/assets/card-asset.json) objects; `min` / `max` constrain card count." + }, + "card_aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio shared across all cards (e.g., '1:1', '1.91:1', '4:5')." + }, + "min_cards": { + "type": "integer", + "minimum": 2, + "description": "Minimum card count (typical: 2 or 3)." + }, + "max_cards": { + "type": "integer", + "description": "Maximum card count (typical: 6, 10, or 35 depending on platform)." + }, + "allowed_card_media_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "Asset types each card's `media` field may carry. Default: ['image']. Polymorphic carousels (Meta) allow ['image', 'video']. Renamed from `allowed_card_asset_types` to disambiguate that this constrains the card's media payload, not the card-asset itself (which is always asset_type: \"card\")." + }, + "allowed_card_asset_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "image", + "video" + ] + }, + "description": "DEPRECATED \u2014 alias for `allowed_card_media_asset_types`. Kept for back-compat; prefer the new field name. Removed in 5.0." + }, + "card_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "card_video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum length of the carousel-level primary text." + }, + "card_headline_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card headline character limit. Governs the `headline` field on each card-asset in the `cards` slot." + }, + "card_description_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-card description character limit. Governs the `description` field on each card-asset in the `cards` slot. Distinct from `card_headline_max_chars`: description is longer body copy (typically 100-500 chars); headline is the short label (typically 25-40 chars)." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Video", + "description": "Direct video file (mp4/webm/mov) hosted by the buyer. Slot: `video_main` (video asset, file or hosted URL), optional `headline`, `brand_name`, `cta`, `companion_banner`, `landing_page_url`. Tracking model: IAB Open Measurement SDK + external impression/click/quartile pixels via universal_macros. Orientation is a parameter (vertical 9:16 / horizontal 16:9 / square 1:1); slot shape includes optional `brand_name` (typical for vertical short-form) and optional `companion_banner` (typical for horizontal instream). Distinct from `video_vast` (VAST tag, inherent VAST event tracking) \u2014 receivers fire impression and click pixels at delivery time.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "video_main", + "asset_type": "video", + "required": true + }, + { + "asset_group_id": "headline", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "primary_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "companion_banner", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_hosted canonical. Buyer ships a video asset (file or hosted URL); optional headline, primary text (long-form caption), CTA (typically constrained via `cta_values`), brand_name (typical for vertical short-form), companion_banner (typical for horizontal instream), and clickthrough URL. Products MAY override or extend the default \u2014 e.g., remove `companion_banner` for short-form vertical, narrow `cta` to a value enum, mark `landing_page_url` as required." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ], + "description": "Video orientation. Vertical = 9:16 (Reels, Stories, Shorts). Horizontal = 16:9 (instream, CTV). Square = 1:1 (in-feed)." + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$", + "description": "Aspect ratio. Inferred from orientation if omitted." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: when both `duration_ms_exact` and `duration_ms_range` ship on the same product, `duration_ms_exact` takes precedence \u2014 buyers MUST validate against the exact value and ignore the range. The range is treated as advisory metadata in that case (e.g., for UI display showing the broader product family). SDKs SHOULD lint a warning when both fields ship; producers SHOULD pick one." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship (see `duration_ms_range` description)." + }, + "video_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "h264", + "h265", + "vp8", + "vp9", + "av1", + "prores" + ] + } + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aac", + "mp3", + "opus", + "pcm" + ] + } + }, + "containers": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp4", + "webm", + "mov" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_file_size_mb": { + "type": "integer", + "minimum": 1 + }, + "frame_rates": { + "type": "array", + "items": { + "type": "number" + } + }, + "captions": { + "type": "string", + "enum": [ + "required", + "recommended", + "not_required" + ] + }, + "om_sdk_required": { + "type": "boolean" + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "primary_text_max_chars": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + } + }, + "companion_banner_widths": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + }, + "description": "Permitted companion banner widths (instream video)." + }, + "companion_banner_heights": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered asset bytes come from. Single shared enum across canonicals. See `image.json#asset_source` for the full semantics. `publisher_host_recorded` is audio-specific and has no defined behavior on video \u2014 adopters MUST select a value appropriate to the canonical." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded video. When `rejected`, the buyer cannot ship a video asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the video." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "VAST Video Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "video_vast" + }, + "params": { + "title": "Canonical Format: VAST Video", + "description": "VAST-tag-delivered video creative. Slot: `vast_tag` (vast asset, URL or inline XML, VAST 2.x-4.x). Tracking model: VAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `expand`, `collapse`, `fullscreen`, `creativeView`, `clickTracking`, `error`. VPAID interactivity via `vpaid_enabled: true` flag. SIMID extensions for interactive video supported as VAST extensions. Orientation is a parameter (vertical / horizontal / square). Distinct from `video_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "vast_tag", + "asset_type": "vast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for video_vast canonical. Buyer ships a VAST tag (URL or inline XML, VAST 2.x-4.x) plus an optional clickthrough URL (which falls back to the VAST `ClickThrough` element when omitted). Tracking events are inherent to VAST and don't require explicit slots." + }, + "orientation": { + "type": "string", + "enum": [ + "vertical", + "horizontal", + "square" + ] + }, + "aspect_ratio": { + "type": "string", + "pattern": "^[0-9]+(\\.[0-9]+)?:[0-9]+(\\.[0-9]+)?$" + }, + "vast_version": { + "type": "string", + "enum": [ + "2.0", + "3.0", + "4.0", + "4.1", + "4.2" + ], + "description": "Required VAST version." + }, + "vpaid_enabled": { + "type": "boolean", + "description": "Whether VPAID interactivity is supported. When true, the VAST tag may carry VPAID JS/Flash payloads." + }, + "vpaid_version": { + "type": "string", + "enum": [ + "1.0", + "2.0" + ] + }, + "simid_supported": { + "type": "boolean", + "description": "Whether IAB SIMID interactive video extensions are supported." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "min_width": { + "type": "integer", + "minimum": 1 + }, + "max_width": { + "type": "integer", + "minimum": 1 + }, + "min_height": { + "type": "integer", + "minimum": 1 + }, + "max_height": { + "type": "integer", + "minimum": 1 + }, + "linear_required": { + "type": "boolean", + "description": "Whether the VAST creative must be linear (non-skippable in-stream)." + }, + "skippable_after_ms": { + "type": "integer", + "minimum": 0, + "description": "When skippable, the buyer-side skip threshold in milliseconds (e.g., 5000 for 5-second skippable pre-roll)." + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0, + "description": "Maximum VAST wrapper redirect depth permitted." + }, + "ssl_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Hosted Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_hosted" + }, + "params": { + "title": "Canonical Format: Hosted Audio", + "description": "Direct audio creative \u2014 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) \u2014 there is no separate manifest 'inputs' map; everything the buyer ships goes in `assets`.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "audio_main", + "asset_type": "audio", + "required": true + }, + { + "asset_group_id": "companion_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "brand_name", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for buyer-uploaded audio. Host-read products override with a `script` (asset_type: text) or `creative_brief` (asset_type: brief) slot in place of `audio_main`, plus `asset_source: 'publisher_host_recorded'` and `buyer_asset_acceptance: 'rejected'`. TTS-from-script products override similarly with `asset_source: 'seller_pre_rendered_from_brief'`." + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship on the same product. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "audio_codecs": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mp3", + "aac", + "wav", + "opus", + "flac" + ] + } + }, + "audio_sample_rates": { + "type": "array", + "items": { + "type": "integer", + "minimum": 1 + } + }, + "audio_channels": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "mono", + "stereo" + ] + } + }, + "min_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "max_bitrate_kbps": { + "type": "integer", + "minimum": 1 + }, + "loudness_lufs": { + "type": "number", + "description": "Required integrated loudness in LUFS (typical: -16 for streaming/podcast, -23 for broadcast). Negative values." + }, + "loudness_tolerance_db": { + "type": "number", + "minimum": 0, + "description": "Permitted deviation from loudness_lufs in dB." + }, + "true_peak_dbfs": { + "type": "number", + "description": "Maximum true-peak level in dBFS (typical: -2)." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "publisher_host_recorded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered audio bytes come from. Single shared enum across canonicals (see `image.json#asset_source` for the full semantics). `publisher_host_recorded`: the publisher's host records the audio (podcast host-read pattern); buyer must use the publisher's build_creative capability. This value is audio-specific." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded audio. When `rejected`, the buyer cannot ship an audio asset directly \u2014 they must use build_creative (or sync_creatives with brief inputs) so the seller produces the audio. Combined with `asset_source`, lets a product declare 'I produce audio from briefs and refuse buyer uploads' (asset_source=`seller_pre_rendered_from_brief`, buyer_asset_acceptance=`rejected`)." + }, + "companion_image_required": { + "type": "boolean" + }, + "companion_image_aspect_ratio": { + "type": "string" + }, + "companion_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "brand_name_max_chars": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "DAAST Audio Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "audio_daast" + }, + "params": { + "title": "Canonical Format: DAAST Audio", + "description": "DAAST-tag-delivered audio creative (audio analog of VAST). Slot: `daast_tag` (daast asset, URL or inline XML). Tracking model: DAAST events inherent to the spec \u2014 `impression`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `start`, `pause`, `resume`, `mute`, `unmute`, `clickTracking`, `error`. Distinct from `audio_hosted` (direct file with external tracking).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "slots": { + "default": [ + { + "asset_group_id": "daast_tag", + "asset_type": "daast", + "required": true + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "Default slots for audio_daast canonical. Buyer ships a DAAST tag (URL or inline XML, 1.0 or 1.1) plus an optional clickthrough URL. Tracking events are inherent to DAAST and don't require explicit slots." + }, + "daast_version": { + "type": "string", + "enum": [ + "1.0", + "1.1" + ] + }, + "duration_ms_range": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 2, + "maxItems": 2, + "description": "[min, max] duration in milliseconds. **Precedence**: `duration_ms_exact` takes precedence when both ship. SDKs SHOULD lint a warning when both fields ship." + }, + "duration_ms_exact": { + "type": "integer", + "minimum": 1, + "description": "When set, duration must equal exactly this value. Takes precedence over `duration_ms_range` when both ship." + }, + "linear_required": { + "type": "boolean" + }, + "max_wrapper_depth": { + "type": "integer", + "minimum": 0 + }, + "ssl_required": { + "type": "boolean" + }, + "companion_image_required": { + "type": "boolean" + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Sponsored Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "sponsored_placement" + }, + "params": { + "title": "Canonical Format: Sponsored Placement (retail-media catalog-driven)", + "description": "Catalog-driven retail-media format. Slot: `source_catalog` (catalog asset \u2014 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** \u2014 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 \u2014 buyer-agent routing).** This canonical is the home for catalog-driven retail-media placements ONLY. The defining feature is the `source_catalog` slot \u2014 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) \u2014 use `native_in_feed` (asset-bundle composition; no catalog).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Single-image or single-video creative \u2014 use `image` or `video_hosted`.\n\nThe earlier broader framing ('any sponsored placement') was too loose for buyer-agent routing \u2014 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).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical covers 4 meaningfully different retail-media adapter contracts (Amazon SP, Criteo SP / CitrusAd SP, Pinterest Collection, generative-per-SKU). Adopter contracts vary; buyers MUST validate per-adapter behavior before routing budget. Promotion to non-experimental gated on the #4592 adapter-contract docs work." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 retail-media catalog placements weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural, not a registry-coverage gap." + }, + "slots": { + "default": [ + { + "asset_group_id": "source_catalog", + "required": true, + "asset_type": "catalog" + }, + { + "asset_group_id": "hero_asset", + "required": false, + "asset_type": "image" + }, + { + "asset_group_id": "landing_page_url", + "required": false, + "asset_type": "url" + } + ] + }, + "supported_catalog_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "product", + "store", + "offering", + "hotel", + "flight", + "vehicle", + "real_estate", + "education", + "destination", + "app", + "job", + "inventory" + ] + }, + "description": "Catalog types this product accepts." + }, + "min_items": { + "type": "integer", + "minimum": 1, + "description": "Minimum catalog item count buyer must supply." + }, + "max_items": { + "type": "integer", + "description": "Maximum items considered for placement." + }, + "fanout_mode": { + "type": "string", + "enum": [ + "per_item", + "multi_item_in_creative", + "single_item" + ], + "description": "How items map to delivery: per_item = one ad per catalog item; multi_item_in_creative = composed multi-item ad (Pinterest Collection, Snap Collection); single_item = one ad showing one item." + }, + "required_catalog_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog item fields the seller requires (e.g., ['title', 'image_url', 'price'])." + }, + "supported_id_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "asin", + "sku", + "gtin", + "offering_id", + "store_id", + "hotel_id", + "flight_id", + "vehicle_id", + "listing_id", + "program_id", + "destination_id", + "app_id", + "job_id" + ] + }, + "description": "Catalog identifier types the placement renders against." + }, + "hero_asset_supported": { + "type": "boolean", + "description": "Whether the buyer can supply a hero/banner asset alongside the catalog (Pinterest Collection pattern)." + }, + "item_production_model": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "How each per-item creative is produced. Covers the same production-source axis as `asset_source` on `image` / `video_hosted` / `audio_hosted` but with a 4-value subset \u2014 drops `publisher_host_recorded` because it's audio-specific and doesn't apply to retail-media catalog placements. SDK codegen MAY share a base enum and narrow per-canonical, or emit two distinct enums; either way the wire values overlap exactly for the 4 retained values. `buyer_uploaded` (default, current Amazon/Criteo/CitrusAd pattern): the buyer's catalog already contains rendered assets per item; the seller composes the placement using those assets. (\"Uploaded\" reads slightly off for catalog-keyed items where the buyer didn't actively upload bytes \u2014 the catalog ingestion already supplied them \u2014 but the semantic is the same: rendered bytes are buyer-supplied, not seller-produced.) `seller_pre_rendered_from_brief`: the buyer ships a brief plus the catalog reference; the seller renders one creative per catalog item from the brief at sync_creatives time. `seller_human_designed`: seller's design team produces per-item renders manually. `agent_synthesized`: AI synthesis pipeline produces per-item renders; pair with `synthesis_nondeterministic: true` for Veo/Sora-class generative video applied per item. Captures the multi-output generative pattern (1 brief \u00d7 N catalog items \u2192 N rendered creatives) under the existing canonical without requiring a separate canonical. Distinct from `fanout_mode`, which describes how items map to delivery slots after rendering." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Native In-Feed Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "native_in_feed" + }, + "params": { + "title": "Canonical Format: Native In-Feed", + "description": "IAB-shaped native creative for in-feed and content-recommendation surfaces. Default slots cover the primary IAB OpenRTB Native 1.2 asset types \u2014 `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 \u2014 visible URL/domain, distinct from clickthrough), `rating` (Data Asset type 3 \u2014 app/product rating), `price` (Data Asset type 6 \u2014 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 \u2014 type 4, downloads \u2014 type 5, saleprice \u2014 type 7, phone_number \u2014 type 8, address \u2014 type 9, desc2 \u2014 type 10, etc.) or to remove slots the surface doesn't render. The publisher's renderer assembles these into its own look-and-feel \u2014 feed card, content-recommendation slot, in-stream native unit. Buyer ships a single asset bundle; the surface chooses presentation.\n\n**Scope (normative \u2014 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) \u2014 use `sponsored_placement` (requires `source_catalog`).\n- Algorithmic surface that picks from a buyer-supplied asset pool (Google PMax, Meta Advantage+) \u2014 use `responsive_creative`.\n- Multi-card carousel \u2014 use `image_carousel`.\n- Video-first native units where the asset is a hosted video file \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": false, + "description": "Stable at 3.1 GA. Shape mirrors IAB OpenRTB Native 1.2 \u2014 the renderer contract is well-established across in-feed native and content-recommendation adopters." + }, + "v1_translatable": { + "default": true, + "description": "Translates to v1 named native formats (e.g., `native_standard`, `native_content`) via the projection registry. Sellers with existing v1 named native formats SHOULD point `v1_format_ref[]` at them." + }, + "slots": { + "default": [ + { + "asset_group_id": "title", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "body_text", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "main_image", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "icon", + "asset_type": "image", + "required": false + }, + { + "asset_group_id": "cta", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "advertiser_name", + "asset_type": "text", + "required": true + }, + { + "asset_group_id": "sponsored_label", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true + }, + { + "asset_group_id": "display_url", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "rating", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "price", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "impression_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "viewability_tracker", + "asset_type": "pixel_tracker", + "required": false + }, + { + "asset_group_id": "click_tracker", + "asset_type": "pixel_tracker", + "required": false + } + ], + "description": "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)." + }, + "title_max_chars": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "body_text_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the body_text slot. IAB native typical: 90 (mainline) to 140 (extended)." + }, + "cta_max_chars": { + "type": "integer", + "minimum": 1, + "description": "Maximum character length for the cta slot. Typical: 15\u201325." + }, + "cta_values": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Permitted CTA values for this product (e.g., ['LEARN_MORE', 'SHOP_NOW', 'SIGN_UP', 'DOWNLOAD']). When set, narrows the cta slot to a closed enum." + }, + "main_image_sizes": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "description": "Accepted (width, height) pairs for the main_image slot. Common IAB native sizes: 1200\u00d7627 (1.91:1), 1080\u00d71080 (1:1), 1080\u00d71350 (4:5)." + }, + "icon_size": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false, + "description": "Required (width, height) for the icon slot when present (typical: 80\u00d780 or 100\u00d7100)." + }, + "max_image_file_size_kb": { + "type": "integer", + "minimum": 1, + "description": "Maximum file size in kilobytes for main_image and icon." + }, + "image_formats": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "jpg", + "jpeg", + "png", + "gif", + "webp" + ] + }, + "description": "Permitted image file formats." + }, + "ssl_required": { + "type": "boolean", + "description": "Whether trackers, landing pages, and image URLs must be served over HTTPS." + }, + "asset_source": { + "type": "string", + "enum": [ + "buyer_uploaded", + "seller_pre_rendered_from_brief", + "seller_human_designed", + "agent_synthesized" + ], + "default": "buyer_uploaded", + "description": "Where the rendered native assets come from. `publisher_host_recorded` is omitted (audio-specific and not meaningful for native). Other values mirror the shared production-source axis used on `image` / `video_hosted`. `buyer_uploaded` (default): buyer ships pre-rendered title/image/body. `seller_pre_rendered_from_brief`: buyer ships a brief, seller renders the native bundle. `agent_synthesized`: AI synthesis pipeline produces title + image + body from a brief; pair with `synthesis_nondeterministic: true` for generative pipelines that can't guarantee in-spec output." + }, + "buyer_asset_acceptance": { + "type": "string", + "enum": [ + "accepted", + "rejected" + ], + "default": "accepted", + "description": "Whether the product accepts buyer-uploaded native assets. When `rejected`, the buyer cannot ship pre-rendered title/image/body \u2014 they must use `build_creative` (or `sync_creatives` with brief inputs) so the seller produces the native bundle from a brief." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Responsive Creative Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "responsive_creative" + }, + "params": { + "title": "Canonical Format: Responsive Creative", + "description": "Buyer supplies a pool of typed assets (multiple headlines, descriptions, images, videos, logos); the surface algorithmically composes combinations per placement. **Composition is algorithmic** \u2014 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.).", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: composition is algorithmic (the surface picks combinations and reports per-asset breakdowns), and there's no clean v1-translatable equivalent. Buyers ship asset pools rather than rendered creatives; the surface's per-impression composition cannot be predicted by `validate_input`. Adopters SHOULD validate behavior per surface (Google PMax vs Meta Advantage+ creative differ meaningfully)." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 algorithmic asset-pool composition (Google PMax / Meta Advantage+ creative) wasn't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "headlines", + "asset_type": "text", + "required": true, + "min": 3, + "max": 15 + }, + { + "asset_group_id": "long_headlines", + "asset_type": "text", + "required": false, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "descriptions", + "asset_type": "text", + "required": true, + "min": 2, + "max": 5 + }, + { + "asset_group_id": "images_landscape", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_square", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "images_vertical", + "asset_type": "image", + "required": false, + "min": 1, + "max": 20 + }, + { + "asset_group_id": "video", + "asset_type": "video", + "required": false, + "min": 0, + "max": 5 + }, + { + "asset_group_id": "logo", + "asset_type": "image", + "required": true, + "min": 1, + "max": 5 + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": true, + "min": 1, + "max": 1 + } + ] + }, + "headlines_min": { + "type": "integer", + "minimum": 0 + }, + "headlines_max": { + "type": "integer", + "minimum": 0 + }, + "headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "long_headlines_min": { + "type": "integer", + "minimum": 0 + }, + "long_headlines_max": { + "type": "integer", + "minimum": 0 + }, + "long_headline_max_chars": { + "type": "integer", + "minimum": 1 + }, + "descriptions_min": { + "type": "integer", + "minimum": 0 + }, + "descriptions_max": { + "type": "integer", + "minimum": 0 + }, + "description_max_chars": { + "type": "integer", + "minimum": 1 + }, + "images_landscape_min": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_max": { + "type": "integer", + "minimum": 0 + }, + "images_landscape_aspect_ratio": { + "type": "string" + }, + "images_square_min": { + "type": "integer", + "minimum": 0 + }, + "images_square_max": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_min": { + "type": "integer", + "minimum": 0 + }, + "images_vertical_max": { + "type": "integer", + "minimum": 0 + }, + "videos_min": { + "type": "integer", + "minimum": 0 + }, + "videos_max": { + "type": "integer", + "minimum": 0 + }, + "video_min_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "video_max_duration_ms": { + "type": "integer", + "minimum": 1 + }, + "logo_min": { + "type": "integer", + "minimum": 0 + }, + "logo_max": { + "type": "integer", + "minimum": 0 + }, + "logo_aspect_ratios": { + "type": "array", + "items": { + "type": "string" + } + }, + "business_name_max_chars": { + "type": "integer", + "minimum": 1 + }, + "asset_image_max_file_size_kb": { + "type": "integer", + "minimum": 1 + }, + "supports_catalog_input": { + "type": "boolean", + "description": "Whether the product can additionally consume a catalog reference (e.g., PMax with product feed)." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Agent Placement Format Declaration", + "properties": { + "format_kind": { + "type": "string", + "const": "agent_placement" + }, + "params": { + "title": "Canonical Format: Agent Placement (AI-surface sponsored placement)", + "description": "**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 set `runtime_status: 'preview'` or `'declared_only'` on the declaration; 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** \u2014 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 \u2014 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 \u2014 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.", + "allOf": [ + { + "title": "Canonical Format Base", + "description": "Shared parameter fields that apply across canonical formats. Each canonical format extends this base with format-specific parameters (dimensions, durations, codecs, slot constraints).", + "type": "object", + "properties": { + "experimental": { + "type": "boolean", + "default": false, + "description": "When true, this canonical (or a seller's specific narrowing of it) may not work as declared \u2014 adopters SHOULD have a v1 fallback ready and SHOULD NOT route production budget without testing. Same semantics as `experimental` on protocols: 'this is shipping but may break, evolve, or fail.' Buyers reading `experimental: true` SHOULD prefer the v1 path when a v1 fallback exists for the same product (via `format_ids` on the parent product or via the v2 declaration's `v1_format_ref`).\n\nThree drivers of `experimental: true`:\n1. **Spec maturity** \u2014 the canonical's tracking model or parameter shape is still being settled (`agent_placement`'s tracking macros, `sponsored_placement`'s per-adapter contracts, `responsive_creative`'s algorithmic composition).\n2. **Adopter runtime gap** \u2014 the seller has declared the canonical in their catalog but their runtime doesn't yet honor it cleanly.\n3. **Custom shapes** \u2014 `format_kind: \"custom\"` is inherently experimental until the working group promotes a `format_shape` to a first-class canonical.\n\nReplaces the earlier `status` enum (`stable | preview | deprecated`) + `runtime_status` enum (`stable | preview | declared_only`) \u2014 two axes with subtle overlap. The single boolean is what buyers actually care about: do I treat this as production-stable or as 'try at my own risk.' Sellers SHOULD set `experimental: true` on canonicals or product declarations that aren't yet production-ready, regardless of which axis (spec, runtime, custom) drives the experimentation. The 6 IAB/VAST/DAAST re-encodings (`image`, `display_tag`, `video_hosted`, `video_vast`, `audio_hosted`, `audio_daast`) default to non-experimental at the canonical level; sellers MAY still mark a specific product declaration experimental (e.g., a beta runtime path for an existing product)." + }, + "deprecated": { + "type": "boolean", + "default": false, + "description": "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." + }, + "v1_translatable": { + "type": "boolean", + "default": true, + "description": "Whether this canonical has any v1 named-format equivalent. `true` (default) \u2014 the canonical is structurally expressible as one or more v1 named formats (IAB display sizes, VAST tags, DAAST tags, etc.); v1\u2192v2 projection via `v1-canonical-mapping.json` is meaningful. `false` \u2014 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 \u2014 seller adds explicit `canonical` field or files a registry entry) vs (b) no v1 path is possible (informational \u2014 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) \u2014 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`." + }, + "since_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals are introduced at minor-version boundaries." + }, + "migration_target_version": { + "type": "string", + "pattern": "^[1-9]\\d*\\.(0|[1-9]\\d*)$", + "description": "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 \u2014 canonicals shift at minor-version boundaries. Absence signals 'no specific target' (omit the field rather than use a placeholder like 'unknown')." + }, + "composition_model": { + "type": "string", + "enum": [ + "deterministic", + "algorithmic" + ], + "description": "Whether the surface composes deterministically (buyer can predict per-slot rendering \u2014 sponsored_placement, image, video) or algorithmically (surface chooses combinations or phrasing \u2014 responsive_creative, agent_placement)." + }, + "provenance_required": { + "type": "boolean", + "description": "When true, the product rejects unsigned synthesized assets. Builders calling build_creative MUST attach a C2PA-compatible provenance manifest attributing synthesis to the creative agent." + }, + "platform_extensions": { + "type": "array", + "description": "Platform-specific extensions narrowing the canonical (pixel ID shapes, conversion event taxonomies, platform-specific CTAs/destinations). Each extension is a URI+digest reference resolved against the bundled `extensions` map in get_products responses or fetched directly.\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 \u2014 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.", + "items": { + "title": "Platform Extension Reference", + "description": "Reference to a platform extension definition. The agent that owns the URI is authoritative for the extension's schema. Buyers fetch the definition once per content digest and cache it. Platform extensions are typically bundled in `get_products` responses under an `extensions` map keyed by `uri@digest`, eliminating the need for a separate fetch.\n\n**Within a single response**, multiple references to the same `uri` MUST carry the same `digest` \u2014 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 \u2014 extension authors revise their schemas, the new digest differs, the cache key changes, and the buyer refetches. Cache by `uri@digest`, not by `uri` alone.", + "type": "object", + "required": [ + "uri", + "digest" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL identifying the extension. `https://` is mandatory \u2014 `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 \u2014 SSRF allowlist, response-size cap, $ref sandbox, schema-compile bounds \u2014 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": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$", + "description": "SHA-256 content digest of the extension definition (sha256:). Used to detect drift \u2014 if the agent revises the extension, the digest changes and cached definitions become invalid." + } + }, + "additionalProperties": true, + "examples": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + }, + "synthesis_nondeterministic": { + "type": "boolean", + "description": "When true, the format's production pipeline is genuinely nondeterministic \u2014 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 \u2014 'seller renders from brief but each retry differs' is just as nondeterministic as Veo). It MUST NOT pair with `buyer_uploaded` (the buyer ships pre-rendered bytes; there's no synthesis step to be nondeterministic about). It MUST NOT pair with `publisher_host_recorded` (the publisher's host produces a deterministic-from-script output even if the human voice varies). When `synthesis_nondeterministic: true` is set with an incompatible source, validators SHOULD reject with a structured error.", + "default": false + }, + "slots": { + "type": "array", + "description": "Programmatic declaration of which canonical asset_group_id slots a manifest targeting this format must (or may) populate. Lets SDK codegen and validators enumerate expected slots without parsing the format's prose description. Each entry references an asset_group_id from the canonical vocabulary registry, paired with an `asset_type` so the validator knows which asset schema to apply. Format-level narrowing parameters that apply across all slots (e.g., flat `headline_max_chars` on responsive_creative) may also live on the format declaration; per-slot constraints (a specific slot's `max_chars` or `max_size_kb`) live on the slot entry.", + "items": { + "type": "object", + "required": [ + "asset_group_id", + "asset_type" + ], + "properties": { + "asset_group_id": { + "type": "string", + "description": "Canonical asset_group_id from /schemas/core/asset-group-vocabulary.json. Non-canonical IDs are valid but trigger soft warnings." + }, + "asset_type": { + "type": "string", + "enum": [ + "image", + "video", + "audio", + "text", + "markdown", + "url", + "html", + "css", + "javascript", + "vast", + "daast", + "webhook", + "brief", + "catalog", + "zip", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ], + "description": "Discriminator selecting the asset schema this slot accepts. SDK codegen uses this to type the slot value. `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 \u2014 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 \u2014 prefer specific types whenever possible." + }, + "required": { + "type": "boolean", + "description": "Whether this slot is required for a valid manifest.", + "default": false + }, + "min": { + "type": "integer", + "minimum": 0, + "description": "Minimum count for repeatable / pool slots." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Maximum count for repeatable / pool slots." + }, + "max_chars": { + "type": "integer", + "minimum": 1, + "description": "Per-slot character limit. 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_size_kb": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "description": { + "type": "string", + "description": "Human-readable description of what the slot expects from the buyer." + }, + "consumed_for_production": { + "type": "boolean", + "default": false, + "description": "Dispatch hint for `build_creative` and v1\u2194v2 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\u2194v2 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." + } + }, + "additionalProperties": true, + "allOf": [ + { + "$comment": "max_chars is valid only for text-shaped asset types; max_size_kb is valid only for binary asset types. A slot MUST NOT carry both; the type-appropriate one is enforced via if/then.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "text", + "markdown", + "brief" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_size_kb" + ] + } + } + }, + { + "if": { + "properties": { + "asset_type": { + "enum": [ + "image", + "video", + "audio", + "zip" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "required": [ + "max_chars" + ] + } + } + }, + { + "$comment": "Non-size asset types (url, catalog, html, css, javascript, webhook, daast, vast, card, object, pixel_tracker, vast_tracker, daast_tracker) reject both \u2014 there's no defined per-slot size semantics on those.", + "if": { + "properties": { + "asset_type": { + "enum": [ + "url", + "catalog", + "html", + "css", + "javascript", + "webhook", + "daast", + "vast", + "card", + "object", + "pixel_tracker", + "vast_tracker", + "daast_tracker" + ] + } + }, + "required": [ + "asset_type" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "max_chars" + ] + }, + { + "required": [ + "max_size_kb" + ] + } + ] + } + } + } + ] + } + }, + "production_window_business_days": { + "type": "integer", + "minimum": 0, + "description": "Typical production turnaround in business days when the format requires seller-side production (e.g., host-recording from a buyer-supplied script). 0 for synchronous (e.g., generative AI); >0 for human-produced (e.g., podcast host-read). Absent when no production is required (buyer uploads complete creative)." + } + }, + "additionalProperties": true, + "x-canonical-policy-required-params-not-enforced": "Canonical format schemas intentionally do NOT mark any of their parameter fields as `required` and use `additionalProperties: true`. The canonical is the loose contract \u2014 describing what parameters MAY narrow it. Products narrow the canonical with concrete values (and may also leave most parameters absent \u2014 buyers infer behavior from the canonical's prose description for any unspecified parameter). This is intentional, not an oversight: enforcing required params on the canonical would force every product to declare every parameter, which is the wrong direction (we want products to declare only the values that differ from sensible canonical defaults). Adopters writing strict validators should validate against the canonical's structural shape (what fields ARE valid) and apply business rules separately for product-level narrowing minima." + } + ], + "properties": { + "experimental": { + "default": true, + "description": "Marked experimental at 3.1 GA: the canonical's tracking model (mention-level impression + attribution, postback shape, cross-surface dedup) is intentionally underspecified for 3.1. Adopters claiming `agent_placement` ship private tracking integrations; buyer agents MUST treat attribution as adapter-defined until the 3.2 tracking-macro spec lands. Promotion to non-experimental gated on the 3.2 tracking-contract spec." + }, + "v1_translatable": { + "default": false, + "description": "Inherently new in v2 \u2014 AI-surface sponsored mentions weren't expressible as v1 named formats. SDKs MUST NOT emit `FORMAT_PROJECTION_FAILED` for products using this canonical; the v1-unreachability is structural." + }, + "slots": { + "default": [ + { + "asset_group_id": "offering_ref", + "asset_type": "text", + "required": false + }, + { + "asset_group_id": "landing_page_url", + "asset_type": "url", + "required": false + } + ], + "description": "agent_placement has minimal buyer-shipped slots \u2014 the surface composes the rendered output from brand context (resolved via the manifest's top-level `brand` BrandRef) plus optional offering_ref and landing_page_url assets. None of these assets are rendered verbatim by the buyer; the agent chooses how to use them." + }, + "output_modality": { + "type": "string", + "enum": [ + "text", + "audio", + "card" + ], + "description": "How the surface presents the mention. `text` = inline text (chat, search snippet). `audio` = TTS-synthesized voice. `card` = structured card with optional image + text." + }, + "max_mention_length_chars": { + "type": "integer", + "minimum": 1, + "description": "For text output: maximum length of the surface-composed mention text." + }, + "max_mention_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "For audio output: maximum duration of the spoken mention in milliseconds." + }, + "supports_offering_reference": { + "type": "boolean", + "description": "Whether the product accepts an offering reference (specific product/service to promote within the mention) in addition to brand context." + }, + "supports_landing_page_url": { + "type": "boolean", + "description": "Whether the surface attaches a landing page URL to the mention (citation, learn-more link)." + }, + "tone_constraints": { + "type": "array", + "items": { + "type": "string" + }, + "description": "**Advisory only.** Buyer-declared brand-voice preferences the surface SHOULD honor (e.g., ['formal', 'no_superlatives']). LLM/agentic surfaces have no protocol-level mechanism to verify enforcement \u2014 adopters that need hard guarantees should rely on brand.json voice declarations and post-mention review rather than this field. Future revisions may tie this to a structured tone vocabulary; for now treat as free-text guidance." + }, + "disclosure_required": { + "type": "boolean", + "description": "Whether the surface must include an explicit sponsorship disclosure label." + } + }, + "additionalProperties": true + } + }, + "required": [ + "format_kind", + "params" + ] + }, + { + "title": "Custom Format Declaration", + "description": "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 \u2014 kept as `type: object` here with `additionalProperties: true` because the canonical schema validates dynamically post-fetch.", + "properties": { + "format_kind": { + "type": "string", + "const": "custom" + }, + "params": { + "type": "object", + "additionalProperties": true, + "description": "Custom shape's params. Validated against the schema fetched from `format_schema.uri` at the cached `format_schema.digest`." + } + }, + "required": [ + "format_kind", + "params" + ] + } + ], + "examples": [ + { + "description": "Meta Reels \u2014 narrows video_hosted (vertical orientation)", + "data": { + "format_kind": "video_hosted", + "params": { + "orientation": "vertical", + "aspect_ratio": "9:16", + "duration_ms_range": [ + 3000, + 90000 + ], + "min_width": 1080, + "min_height": 1920, + "max_file_size_mb": 200, + "video_codecs": [ + "h264" + ], + "audio_codecs": [ + "aac" + ], + "headline_max_chars": 25, + "primary_text_max_chars": 72, + "captions": "recommended", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "DOWNLOAD", + "SIGN_UP" + ], + "composition_model": "deterministic", + "platform_extensions": [ + { + "uri": "https://creative.adcontextprotocol.org/translated/meta/extensions/meta_pixel", + "digest": "sha256:a3f5b7c9d8e2f1a4b6c8d0e2f4a6b8c0d2e4f6a8b0c2d4e6f8a0b2c4d6e8f0a2" + } + ] + } + } + }, + { + "description": "IAB Medium Rectangle (300x250) \u2014 narrows image", + "data": { + "format_kind": "image", + "params": { + "width": 300, + "height": 250, + "max_file_size_kb": 200, + "image_formats": [ + "jpg", + "png", + "gif" + ], + "ssl_required": true, + "composition_model": "deterministic", + "cta_values": [ + "LEARN_MORE", + "SHOP_NOW", + "GET_OFFER" + ] + } + } + }, + { + "description": "Podcast 30s host-read \u2014 narrows audio_hosted with a `script` slot the seller's host reads verbatim. No separate `inputs` map; the script lives in the manifest's `assets` like any other text asset.", + "data": { + "format_kind": "audio_hosted", + "params": { + "duration_ms_exact": 30000, + "audio_codecs": [ + "mp3", + "aac" + ], + "audio_sample_rates": [ + 44100, + 48000 + ], + "audio_channels": [ + "stereo" + ], + "loudness_lufs": -16, + "asset_source": "publisher_host_recorded", + "buyer_asset_acceptance": "rejected", + "composition_model": "deterministic", + "slots": [ + { + "asset_group_id": "script", + "required": true, + "asset_type": "text", + "max_chars": 800 + }, + { + "asset_group_id": "offering_ref", + "required": false, + "asset_type": "text" + } + ], + "production_window_business_days": 7 + } + } + }, + { + "description": "NYTimes Homepage Takeover \u2014 custom format_kind, classified against the multi_placement_takeover format_shape, with format_schema pointing at NYTimes's hosted schema. Buyer agents fetch the schema by uri@digest (cached, immutable) and validate the manifest structurally. `canonical_formats_only: true` is required for custom declarations \u2014 no v1 named format can express the multi-placement shape.", + "data": { + "format_kind": "custom", + "canonical_formats_only": true, + "format_shape": "multi_placement_takeover", + "format_schema": { + "uri": "https://nytimes.example/schemas/formats/homepage_takeover_v3", + "digest": "sha256:e1d4f6a9c2b5e8d1f4a7c0b3e6d9f2a5c8b1e4d7f0a3c6b9e2d5f8a1c4b7e0a3" + }, + "format_option_id": "nytimes_homepage_takeover_premium", + "display_name": "Homepage Takeover \u2014 Premium Sponsorship", + "applies_to_channels": [ + "display", + "olv" + ], + "params": { + "components": [ + { + "placement_type": "homepage_skin", + "required": true + }, + { + "placement_type": "preroll_video", + "required": true + }, + { + "placement_type": "sponsorship_lockup", + "required": true + } + ], + "exclusivity_window_hours": 24, + "ssl_required": true + } + } + } + ] + }, + "minItems": 1 + } + }, + "required": [ + "kind", + "placement_id", + "mode" + ], + "$comment": "The anyOf(name OR publisher_domain) is a schema-local proxy for the cross-document invariant: publisher-referenced placements can omit name only because {publisher_domain, placement_id} resolves to an adagents.json placement that supplies the name. Seller-inline placements carry name directly.", + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "publisher_domain" + ] + } + ], + "allOf": [ + { + "$comment": "Public product placements must not leak seller-private delivery mapping fields. Consumers that detect this leak should surface PRIVATE_FIELD_IN_PUBLIC_PLACEMENT for monitoring.", + "not": { + "anyOf": [ + { + "required": [ + "visibility" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "origin" + ] + }, + { + "required": [ + "delivery_mappings" + ] + } + ] + } + }, + { + "if": { + "properties": { + "kind": { + "type": "string", + "const": "publisher_ref" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "publisher_domain" + ] + } + }, + { + "if": { + "properties": { + "kind": { + "type": "string", + "const": "seller_inline" + } + }, + "required": [ + "kind" + ] + }, + "then": { + "required": [ + "name" + ] + } + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "delivery_type": { + "$ref": "#/$defs/DeliveryType" + }, + "exclusivity": { + "$ref": "#/$defs/Exclusivity" + }, + "pricing_options": { + "type": "array", + "description": "Available pricing models for this product", + "items": { + "title": "Pricing Option", + "description": "A pricing model option offered by a publisher for a product. Discriminated by pricing_model field. If fixed_price is present, it's fixed pricing. If absent, it's auction-based (floor_price and price_guidance optional). Bid-based auction models may also include max_bid as a boolean signal to interpret bid_price as a buyer ceiling instead of an exact honored price.", + "discriminator": { + "propertyName": "pricing_model" + }, + "oneOf": [ + { + "title": "CPM Pricing Option", + "description": "Cost Per Mille (cost per 1,000 impressions) pricing. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpm", + "description": "Cost per 1,000 impressions" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "vCPM Pricing Option", + "description": "Viewable Cost Per Mille (cost per 1,000 viewable impressions) pricing - MRC viewability standard. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "vcpm", + "description": "Cost per 1,000 viewable impressions (MRC standard)" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per unit. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CPC Pricing Option", + "description": "Cost Per Click pricing. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpc", + "description": "Cost per click" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per click. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CPCV Pricing Option", + "description": "Cost Per Completed View (100% video/audio completion) pricing. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpcv", + "description": "Cost per completed view (100% completion)" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per completed view. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CPV Pricing Option", + "description": "Cost Per View (at publisher-defined threshold) pricing for video/audio. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpv", + "description": "Cost per view at threshold" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per view. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "max_bid": { + "type": "boolean", + "description": "When true, bid_price is interpreted as the buyer's maximum willingness to pay (ceiling) rather than an exact price. Sellers may optimize actual clearing prices between floor_price and bid_price based on delivery pacing. When false or absent, bid_price (if provided) is the exact bid/price to honor.", + "default": false + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "type": "object", + "description": "CPV-specific parameters defining the view threshold", + "properties": { + "view_threshold": { + "oneOf": [ + { + "type": "number", + "description": "Percentage completion threshold (0.0 to 1.0, e.g., 0.5 = 50%)", + "minimum": 0, + "maximum": 1 + }, + { + "type": "object", + "description": "Time-based view threshold", + "properties": { + "duration_seconds": { + "type": "integer", + "description": "Seconds of viewing required", + "minimum": 1 + } + }, + "required": [ + "duration_seconds" + ], + "additionalProperties": true + } + ] + } + }, + "required": [ + "view_threshold" + ], + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency", + "parameters" + ], + "additionalProperties": true + }, + { + "title": "CPP Pricing Option", + "description": "Cost Per Point (Gross Rating Point) pricing for TV and audio campaigns. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpp", + "description": "Cost per Gross Rating Point" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per rating point. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "type": "object", + "description": "CPP-specific parameters for demographic targeting", + "properties": { + "demographic_system": { + "$ref": "#/$defs/DemographicSystem" + }, + "demographic": { + "type": "string", + "description": "Target demographic code within the specified demographic_system (e.g., P18-49 for Nielsen, ABC1 Adults for BARB)" + }, + "min_points": { + "type": "number", + "description": "Minimum GRPs/TRPs required", + "minimum": 0 + } + }, + "required": [ + "demographic" + ], + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency", + "parameters" + ], + "additionalProperties": true + }, + { + "title": "CPA Pricing Option", + "description": "Cost Per Acquisition pricing. Advertiser pays a fixed price when a specified conversion event occurs. The event_type field declares which event triggers billing (e.g., purchase, lead, app_install).", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "cpa", + "description": "Cost per acquisition (conversion event)" + }, + "event_type": { + "allOf": [ + { + "$ref": "#/$defs/EventType" + } + ], + "description": "The conversion event type that triggers billing (e.g., purchase, lead, app_install)" + }, + "custom_event_name": { + "type": "string", + "description": "Name of the custom event when event_type is 'custom'. Required when event_type is 'custom', ignored otherwise." + }, + "event_source_id": { + "type": "string", + "description": "When present, only events from this specific event source count toward billing. Allows different CPA rates for different sources (e.g., online vs in-store purchases). Must match an event source configured via sync_event_sources." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Fixed price per acquisition in the specified currency", + "exclusiveMinimum": 0 + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "event_type", + "currency", + "fixed_price" + ], + "additionalProperties": true + }, + { + "title": "Flat Rate Pricing Option", + "description": "Flat rate pricing for sponsorships, takeovers, and DOOH exclusive placements. A fixed total cost regardless of delivery volume. For duration-scaled pricing (rate \u00d7 time units), use the `time` model instead. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "flat_rate", + "description": "Fixed cost regardless of delivery volume" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Flat rate cost. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "title": "DoohParameters", + "description": "DOOH inventory allocation parameters. Sponsorship and takeover flat_rate options omit this field entirely \u2014 only include for digital out-of-home inventory.", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "dooh", + "description": "Discriminator identifying this as DOOH parameters" + }, + "sov_percentage": { + "type": "number", + "description": "Guaranteed share of voice as a percentage (0-100)", + "minimum": 0, + "maximum": 100 + }, + "loop_duration_seconds": { + "type": "integer", + "description": "Duration of the ad loop rotation in seconds", + "minimum": 1 + }, + "min_plays_per_hour": { + "type": "integer", + "description": "Minimum number of plays per hour guaranteed", + "minimum": 1 + }, + "venue_package": { + "type": "string", + "description": "Named collection of screens included in this buy" + }, + "duration_hours": { + "type": "number", + "description": "Duration of the DOOH slot in hours (e.g., 24 for a full-day takeover)", + "minimum": 0 + }, + "daypart": { + "type": "string", + "description": "Named daypart for this slot (e.g., morning_commute, evening_rush)" + }, + "estimated_impressions": { + "type": "integer", + "description": "Estimated audience impressions for this slot (informational, not a delivery guarantee)", + "minimum": 0 + } + }, + "required": [ + "type" + ], + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency" + ], + "additionalProperties": true + }, + { + "title": "Time-Based Pricing Option", + "description": "Cost per time unit (hour, day, week, or month) - rate scales with campaign duration. If fixed_price is present, it's fixed pricing. If absent, it's auction-based.", + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Unique identifier for this pricing option within the product" + }, + "pricing_model": { + "type": "string", + "const": "time", + "description": "Cost per time unit - rate scales with campaign duration" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": [ + "USD", + "EUR", + "GBP", + "JPY" + ] + }, + "fixed_price": { + "type": "number", + "description": "Cost per time unit. If present, this is fixed pricing. If absent, auction-based.", + "minimum": 0 + }, + "floor_price": { + "type": "number", + "description": "Minimum acceptable bid per time unit for auction pricing (mutually exclusive with fixed_price). Bids below this value will be rejected.", + "minimum": 0 + }, + "price_guidance": { + "description": "Pricing guidance for auction-based bidding. Helps buyers calibrate bids with historical percentiles.", + "title": "Price Guidance", + "type": "object", + "properties": { + "p25": { + "type": "number", + "description": "25th percentile of recent winning bids", + "minimum": 0 + }, + "p50": { + "type": "number", + "description": "Median of recent winning bids", + "minimum": 0 + }, + "p75": { + "type": "number", + "description": "75th percentile of recent winning bids", + "minimum": 0 + }, + "p90": { + "type": "number", + "description": "90th percentile of recent winning bids", + "minimum": 0 + } + }, + "additionalProperties": true + }, + "parameters": { + "type": "object", + "description": "Time-based pricing parameters", + "required": [ + "time_unit" + ], + "properties": { + "time_unit": { + "type": "string", + "enum": [ + "hour", + "day", + "week", + "month" + ], + "description": "The time unit for pricing. Total cost = fixed_price \u00d7 number of time_units in the campaign flight." + }, + "min_duration": { + "type": "integer", + "minimum": 1, + "description": "Minimum booking duration in time_units" + }, + "max_duration": { + "type": "integer", + "minimum": 1, + "description": "Maximum booking duration in time_units. Must be >= min_duration when both are present." + } + }, + "additionalProperties": true + }, + "min_spend_per_package": { + "type": "number", + "description": "Minimum spend requirement per package using this pricing option, in the specified currency", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "eligible_adjustments": { + "type": "array", + "description": "Adjustment kinds applicable to this pricing option. Tells buyer agents which adjustments are available before negotiation. When absent, no adjustments are pre-declared \u2014 the buyer should check price_breakdown if present.", + "items": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "uniqueItems": true + } + }, + "required": [ + "pricing_option_id", + "pricing_model", + "currency", + "parameters" + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + }, + "forecast": { + "title": "Delivery Forecast", + "description": "Forecasted delivery metrics for this product. Gives buyers an estimate of expected performance before requesting a proposal.", + "type": "object", + "properties": { + "points": { + "type": "array", + "description": "Forecasted delivery data points. For spend curves (default), points at ascending budget levels show how metrics scale with spend. For availability forecasts, points represent total available inventory independent of budget. See forecast_range_unit for interpretation.", + "items": { + "title": "Forecast Point", + "description": "A forecast data point. When budget is present, the point pairs a spend level with expected delivery \u2014 multiple points at ascending budgets form a curve. When budget is omitted, the point represents total available inventory for the requested targeting and dates, independent of spend.", + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 128, + "description": "Human-readable name for this forecast point. Required when forecast_range_unit is 'package' so buyer agents can identify and reference individual packages. Optional for other forecast types.", + "examples": [ + "Primetime", + "Morning Drive", + "Large Format Transit" + ] + }, + "budget": { + "type": "number", + "description": "Budget amount for this forecast point. Required for spend curves; omit for availability forecasts where the metrics represent total available inventory. For allocation-level forecasts, this is the absolute budget for that allocation (not the percentage). For proposal-level forecasts, this is the total proposal budget. When omitted, use metrics.spend to express the estimated cost of the available inventory.", + "minimum": 0 + }, + "metrics": { + "type": "object", + "description": "Forecasted metric values. Keys are forecastable-metric enum values for delivery/engagement or event-type enum values for outcomes. Values are ForecastRange objects (low/mid/high). Use { \"mid\": value } for point estimates. When budget is present, these are the expected metrics at that spend level. When budget is omitted, these represent total available inventory \u2014 use spend to express the estimated cost. Additional keys beyond the documented properties are allowed for event-type values (purchase, lead, app_install, etc.).", + "properties": { + "audience_size": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "reach": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "frequency": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "clicks": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "spend": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "completed_views": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "grps": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "engagements": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "follows": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "saves": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "profile_visits": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "measured_impressions": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "downloads": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + }, + "plays": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + }, + "additionalProperties": { + "title": "Forecast Range", + "description": "A forecast value with optional confidence bounds. Either mid (point estimate) or both low and high (range) must be provided. mid represents the most likely outcome. low and high represent conservative and optimistic estimates. All three can be provided together.", + "type": "object", + "properties": { + "low": { + "type": "number", + "description": "Conservative (low-end) forecast value", + "minimum": 0 + }, + "mid": { + "type": "number", + "description": "Expected (most likely) forecast value", + "minimum": 0 + }, + "high": { + "type": "number", + "description": "Optimistic (high-end) forecast value", + "minimum": 0 + } + }, + "anyOf": [ + { + "required": [ + "mid" + ] + }, + { + "required": [ + "low", + "high" + ] + } + ], + "additionalProperties": true + } + } + }, + "required": [ + "metrics" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "forecast_range_unit": { + "$ref": "#/$defs/ForecastRangeUnit" + }, + "method": { + "$ref": "#/$defs/ForecastMethod" + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for monetary values in this forecast (spend, budget)" + }, + "demographic_system": { + "$ref": "#/$defs/DemographicSystem" + }, + "demographic": { + "type": "string", + "description": "Target demographic code within the specified demographic_system. For Nielsen: P18-49, M25-54, W35+. For BARB: ABC1 Adults, 16-34. For AGF: E 14-49.", + "examples": [ + "P18-49", + "A25-54", + "W35+", + "M18-34" + ] + }, + "measurement_source": { + "type": "string", + "maxLength": 64, + "pattern": "^[a-z0-9_]+$", + "description": "Third-party measurement provider whose data was used to produce this forecast. Distinct from demographic_system, which specifies demographic notation \u2014 measurement_source identifies whose data produced the forecast numbers. Should be present when measured_impressions is used. Lowercase slug format.", + "examples": [ + "nielsen", + "videoamp", + "comscore", + "geopath", + "barb", + "agf", + "oztam", + "kantar", + "barc", + "route", + "rajar", + "triton" + ] + }, + "reach_unit": { + "$ref": "#/$defs/ReachUnit" + }, + "generated_at": { + "type": "string", + "format": "date-time", + "description": "When this forecast was computed" + }, + "valid_until": { + "type": "string", + "format": "date-time", + "description": "When this forecast expires. After this time, the forecast should be refreshed. Forecast expiry does not affect proposal executability." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "points", + "method", + "currency" + ], + "additionalProperties": true + }, + "outcome_measurement": { + "title": "Outcome Measurement (Deprecated)", + "description": "**Deprecated as of this minor.** Outcome capabilities (incremental sales lift, brand lift, foot traffic, etc.) are now declared via `reporting_capabilities.available_metrics` (the same path used for impressions, conversions, ROAS) with `qualifier.attribution_methodology` and `qualifier.attribution_window` carrying the methodology and window on commit. New implementations SHOULD use the unified pattern; this field is retained for one-minor backwards compatibility and removed at the next major. See `outcome-measurement.json` description for migration guidance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of measurement", + "examples": [ + "incremental_sales_lift", + "brand_lift", + "foot_traffic" + ] + }, + "attribution": { + "type": "string", + "description": "Attribution methodology", + "examples": [ + "deterministic_purchase", + "probabilistic" + ] + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Attribution window as a structured duration (e.g., {\"interval\": 30, \"unit\": \"days\"})." + }, + "reporting": { + "type": "string", + "description": "Reporting frequency and format", + "examples": [ + "weekly_dashboard", + "real_time_api" + ] + } + }, + "required": [ + "type", + "attribution", + "reporting" + ], + "additionalProperties": true + }, + "delivery_measurement": { + "type": "object", + "description": "Measurement vendors and methodology for delivery metrics. The buyer accepts the declared vendors as the source of truth for the buy. When absent, buyers should apply their own measurement defaults. Senders SHOULD populate `vendors` (structured BrandRef array) for new implementations; the legacy `provider` string field is deprecated and retained for one-minor backwards compatibility.", + "properties": { + "vendors": { + "type": "array", + "description": "Measurement vendors used for this product, as structured `BrandRef` identities. Multiple entries when multiple vendors play different roles (e.g., the ad server plus a separate viewability vendor like IAS or DV; or a retail-media seller plus a third-party retail measurement vendor like Circana or NielsenIQ). Each vendor's `brand.json` `agents[type='measurement']` is the discovery anchor; metric definitions live on the agent's `get_adcp_capabilities.measurement.metrics[]` block. Distinct from `performance_standards[].vendor` which carries vendor identity for *committed* metrics with thresholds \u2014 this field carries vendor identity for the overall measurement story, including non-committed-but-reported metrics.", + "items": { + "title": "Brand Reference", + "description": "Reference to a brand by domain and optional brand_id. The domain hosts /.well-known/brand.json or is registered in the brand registry. For single-brand domains, brand_id can be omitted. For house-of-brands domains, brand_id identifies the specific brand.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "minItems": 1 + }, + "provider": { + "type": "string", + "description": "**Deprecated as of this minor.** Free-form measurement provider description (e.g., 'Google Ad Manager with IAS viewability', 'Nielsen DAR', 'Geopath for DOOH impressions'). New implementations SHOULD use the structured `vendors` field instead. Retained for one-minor backwards compatibility; removed at the next major. When both `vendors` and `provider` are present, consumers MUST use `vendors` for vendor identity and treat `provider` as informational text." + }, + "notes": { + "type": "string", + "description": "Additional details about measurement methodology in plain language (e.g., 'MRC-accredited viewability. 50% in-view for 1s display / 2s video', 'Panel-based demographic measurement updated monthly'). Free-form prose for context that doesn't fit the structured `vendors` field." + } + } + }, + "measurement_terms": { + "title": "Measurement Terms", + "description": "Seller's default billing measurement and makegood terms. Declares who counts the billing metric and what remedies apply when thresholds are breached. Buyers may propose different terms at media buy creation \u2014 sellers accept, reject (TERMS_REJECTED), or adjust per their policy.", + "type": "object", + "properties": { + "billing_measurement": { + "type": "object", + "description": "Which vendor's count of the billing metric governs invoicing. The billing metric is determined by the pricing_model on the selected pricing_option (e.g., impressions for CPM, completed views for CPCV).", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor whose measurement of the billing metric is authoritative for invoicing (e.g., { domain: 'campaignmanager.google.com' } for buyer's DCM, { domain: 'admanager.google.com' } for seller's GAM).", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "max_variance_percent": { + "type": "number", + "minimum": 0, + "exclusiveMaximum": 100, + "description": "Maximum acceptable variance between the billing vendor's count and the other party's count before resolution is triggered (e.g., 10 means a 10% divergence triggers review)." + }, + "measurement_window": { + "type": "string", + "description": "Which measurement maturation stage the billing metric is reconciled against. References a window_id from the product's reporting_capabilities.measurement_windows. Examples: 'c7' for broadcast TV guarantees (live + 7 days DVR), 'final' for DOOH after IVT/fraud-check processing, 'post_sivt' for digital after sophisticated invalid-traffic filtering, 'downloads_30d' for podcast. When absent, billing is based on the seller's standard reporting without windowed maturation.", + "examples": [ + "live", + "c3", + "c7", + "tentative", + "final", + "post_ivt", + "post_sivt", + "downloads_30d" + ] + }, + "finalization_deadline_hours": { + "type": "integer", + "minimum": 0, + "description": "Maximum hours by which the authoritative party MUST publish a final record (`is_final: true` / `finalized_at` on `get_media_buy_delivery`, or `final: true` / `finalized_at` on `report_usage`). **Anchor:** when `measurement_window` is set, hours are counted from the close of that window (e.g., 240h after `c7` close = ~10 days after the 7-day DVR accumulation completes); when `measurement_window` is absent, hours are counted from `reporting_period.end`. Picking a single anchor avoids ambiguity for windowed channels where `reporting_period.end` and window close differ by days. The deadline applies to whichever party is named in `vendor` \u2014 seller, buyer, or third-party vendor \u2014 symmetrically. When the deadline elapses without a final record, the counterparty MAY fall back to its own attestation for invoicing (seller falls back to seller-attested numbers via `get_media_buy_delivery`; buyer falls back to a buyer-attested `report_usage` push), and the breach is treated like any other measurement-terms breach under `makegood_policy`. Absent means no contractual deadline \u2014 finalization is best-effort and disagreements resolve out of band." + } + }, + "required": [ + "vendor" + ], + "additionalProperties": true + }, + "makegood_policy": { + "type": "object", + "description": "Remedies available when a performance standard or billing measurement variance is breached. Seller declares which remedy types they support. When a breach occurs, the seller proposes a remedy from this menu; the buyer accepts or disputes.", + "properties": { + "available_remedies": { + "type": "array", + "description": "Remedy types the seller supports. Ordered by seller preference (first = preferred). Seller proposes from this list when a breach occurs; buyer accepts or disputes.", + "items": { + "$ref": "#/$defs/MakegoodRemedy" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "available_remedies" + ], + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "performance_standards": { + "type": "array", + "description": "Seller's default performance standards for this product: viewability, IVT, completion rate, brand safety, attention score. Buyers may propose different standards at media buy creation. When absent, no structured performance standards apply.", + "items": { + "title": "Performance Standard", + "description": "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).", + "type": "object", + "properties": { + "metric": { + "$ref": "#/$defs/PerformanceStandardMetric" + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "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." + }, + "standard": { + "$ref": "#/$defs/ViewabilityStandard" + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor measuring this metric (e.g., { domain: 'doubleverify.com' }). The vendor's brand.json agents array (type: 'measurement') is the discovery point for their measurement agent. When specified on a confirmed package, creatives MUST include tracker_script or tracker_pixel assets from this vendor.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + } + }, + "required": [ + "metric", + "threshold", + "vendor" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "cancellation_policy": { + "title": "Cancellation Policy", + "description": "Cancellation terms for this product. Declares the minimum notice period required before cancellation takes effect and any penalties for insufficient notice. Relevant for guaranteed delivery products. Buyers accept these terms by creating a media buy against the product.", + "type": "object", + "properties": { + "notice_period": { + "title": "Duration", + "description": "Minimum notice period before cancellation takes effect (e.g., { interval: 30, unit: 'days' }). A guaranteed buy canceled without sufficient notice incurs the declared cancellation fee.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + }, + "cancellation_fee": { + "type": "object", + "description": "Fee applied when the notice period is not met.", + "properties": { + "type": { + "type": "string", + "enum": [ + "percent_remaining", + "full_commitment", + "fixed_fee", + "none" + ], + "description": "Fee calculation method. 'percent_remaining': percentage of remaining uncommitted spend. 'full_commitment': buyer owes the full committed budget regardless of delivery. 'fixed_fee': flat monetary amount. 'none': no financial fee (cancellation with notice is free)." + }, + "rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fee rate as a decimal proportion of remaining committed spend. Required when type is 'percent_remaining' (e.g., 0.5 means 50% of remaining spend)." + }, + "amount": { + "type": "number", + "minimum": 0, + "description": "Fixed fee amount in the buy's currency. Required when type is 'fixed_fee'." + } + }, + "required": [ + "type" + ], + "additionalProperties": true + } + }, + "required": [ + "notice_period", + "cancellation_fee" + ], + "additionalProperties": true + }, + "allowed_actions": { + "type": "array", + "description": "Actions buyers may perform on buys created against this product, scoped to statuses and modes. Advisory template \u2014 the authoritative per-buy capability is `available_actions[]` on the buy response, which resolves modes against current buy state, account tier, and negotiated terms. Buyers SHOULD use this for pre-flight product selection (\"which products let me self-serve cancel within 72hr?\") and read `available_actions[]` for runtime decisions. The array is uniquely keyed by `action` \u2014 sellers MUST NOT emit two entries with the same `action` value. Absence means the seller has not declared a structured action surface for this product \u2014 buyers fall back to `valid_actions[]` on buy responses for the flat string vocabulary.", + "items": { + "title": "Product Allowed Action", + "description": "An action a seller declares as allowed on buys created against this product, scoped to the buy statuses where the action is permitted and the modes available. Advisory template only \u2014 the authoritative per-buy resolution lives in `available_actions[]` on the buy response (which may diverge from the product template based on negotiated terms, account tier, or buy-level overrides). The containing `allowed_actions[]` array is uniquely keyed by `action`; sellers MUST NOT emit two entries with the same `action` value. JSON Schema `uniqueItems` only catches structurally identical objects, so validators MUST enforce action-uniqueness separately.", + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/MediaBuyValidAction" + }, + "modes": { + "type": "array", + "description": "Modes available for this action on this product. A product may declare multiple modes (for example `self_serve` within tolerances, escalating to `requires_approval` outside) \u2014 the buy-side `available_actions[].mode` resolves to the singular mode in effect at mutation time. SDKs that see multiple modes MUST NOT assume which one will fire; they must read the resolved `mode` on the buy.", + "items": { + "$ref": "#/$defs/MediaBuyActionMode" + }, + "minItems": 1, + "uniqueItems": true + }, + "allowed_statuses": { + "type": "array", + "description": "Media buy statuses in which this action is permitted. When absent, the action is permitted in all non-terminal statuses (`pending_creatives`, `pending_start`, `active`, `paused`).", + "items": { + "$ref": "#/$defs/MediaBuyStatus" + }, + "minItems": 1, + "uniqueItems": true + }, + "sla": { + "title": "SLA Window", + "description": "Optional SLA commitment for this action on this product. Absence means no commitment.", + "type": "object", + "properties": { + "response_max": { + "type": "string", + "description": "Maximum time from when the buyer issues the action to when the seller acknowledges receipt (mode-appropriate: synchronous response for self_serve, queue ack for requires_approval, proposal task creation for requires_proposal). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT5M", + "PT4H", + "P1D" + ] + }, + "completion_max": { + "type": "string", + "description": "Maximum time from buyer issuing the action to the seller completing it (mutation applied, proposal finalized, approval resolved). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT1H", + "PT24H", + "P2D" + ] + } + }, + "additionalProperties": false + }, + "terms_ref": { + "type": "string", + "description": "Optional pointer into buy-terms negotiation (forward-references the buy-terms namespace landing via separate RFC). When present, the named term governs cancellation policy, makegoods, or other commercial remedies tied to this action. Schema accepts any string for now and will tighten to a structured reference when the buy-terms RFC ships." + } + }, + "required": [ + "action", + "modes" + ], + "additionalProperties": false + }, + "minItems": 1, + "uniqueItems": true + }, + "reporting_capabilities": { + "title": "Reporting Capabilities", + "description": "Reporting capabilities available for a product", + "type": "object", + "properties": { + "available_reporting_frequencies": { + "type": "array", + "description": "Supported reporting frequency options", + "items": { + "$ref": "#/$defs/ReportingFrequency" + }, + "minItems": 1, + "uniqueItems": true + }, + "expected_delay_minutes": { + "type": "integer", + "description": "Expected delay in minutes before reporting data becomes available (e.g., 240 for 4-hour delay)", + "minimum": 0, + "examples": [ + 240, + 300, + 1440 + ] + }, + "timezone": { + "type": "string", + "description": "Timezone for reporting periods. Use 'UTC' or IANA timezone (e.g., 'America/New_York'). Critical for daily/monthly frequency alignment.", + "examples": [ + "UTC", + "America/New_York", + "Europe/London", + "America/Los_Angeles" + ] + }, + "supports_webhooks": { + "type": "boolean", + "description": "Whether this product supports webhook-based reporting notifications" + }, + "available_metrics": { + "type": "array", + "description": "Metrics available in reporting. Impressions and spend are always implicitly included. When a creative format declares reported_metrics, buyers receive the intersection of these product-level metrics and the format's reported_metrics.", + "items": { + "$ref": "#/$defs/AvailableMetric" + }, + "uniqueItems": true, + "examples": [ + [ + "impressions", + "spend", + "clicks", + "completed_views" + ], + [ + "impressions", + "spend", + "conversions" + ] + ] + }, + "vendor_metrics": { + "type": "array", + "description": "Vendor-defined metrics this product can report, beyond the closed `available_metrics` enum. Each entry is a pointer (`{ vendor, metric_id }`) into the vendor's metric catalog \u2014 the canonical definition (standard alignment, accreditations, methodology, unit, human-readable description) lives at the vendor's `get_adcp_capabilities.measurement.metrics[]`, queried once per vendor when needed. Use this for proprietary metrics like attention scores, emissions, panel-based demographics, or platform-native social metrics not yet in the standard enum. Sellers populate values in delivery via `delivery-metrics.json#/properties/vendor_metric_values`. The metric is identified by the tuple `(vendor, metric_id)`; identifiers are namespaced by the vendor, so the same `metric_id` may mean different things in different vendors' vocabularies. Semantic uniqueness key is `(vendor.domain, vendor.brand_id, metric_id)`; sellers MUST de-duplicate before emission and MUST NOT declare the same vendor metric twice. Buyers MAY treat duplicate `(vendor, metric_id)` rows as a seller-side conformance bug. (JSON Schema `uniqueItems` is not used here because BrandRef carries optional fields whose absence/presence would defeat deep-equal \u2014 uniqueness is on the semantic key, enforced at build/validation time on the seller side.) Promotion path: when the industry converges on a metric via a published standard, the spec adds it to the closed `available_metrics` enum and the vendor extensions become historical aliases.", + "items": { + "type": "object", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. The vendor's `brand.json` is the discovery anchor for the measurement agent (entry with `type: 'measurement'` in the `agents[]` array); the metric's standard alignment, accreditations, and methodology live at that agent's `get_adcp_capabilities.measurement.metrics[]` and are not duplicated inline here.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary (e.g., `attention_units`, `gco2e_per_impression`, `demographic_reach`).", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + } + }, + "required": [ + "vendor", + "metric_id" + ], + "additionalProperties": false + } + }, + "supports_creative_breakdown": { + "type": "boolean", + "description": "Whether this product supports creative-level metric breakdowns in delivery reporting (by_creative within by_package)" + }, + "supports_keyword_breakdown": { + "type": "boolean", + "description": "Whether this product supports keyword-level metric breakdowns in delivery reporting (by_keyword within by_package)" + }, + "supports_geo_breakdown": { + "title": "Geographic Breakdown Support", + "description": "Geographic breakdown support for this product. Declares which geo levels and systems are available for by_geo reporting within by_package.", + "type": "object", + "properties": { + "country": { + "type": "boolean", + "description": "Supports country-level geo breakdown (ISO 3166-1 alpha-2)" + }, + "region": { + "type": "boolean", + "description": "Supports region/state-level geo breakdown (ISO 3166-2)" + }, + "metro": { + "type": "object", + "description": "Metro area breakdown support. Keys are metro-system enum values; true means supported.", + "propertyNames": { + "$ref": "#/$defs/MetroAreaSystem" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "postal_area": { + "type": "object", + "description": "Postal area breakdown support. Keys are postal-system enum values; true means supported.", + "propertyNames": { + "$ref": "#/$defs/PostalCodeSystem" + }, + "additionalProperties": { + "type": "boolean" + } + } + }, + "additionalProperties": false + }, + "supports_device_type_breakdown": { + "type": "boolean", + "description": "Whether this product supports device type breakdowns in delivery reporting (by_device_type within by_package)" + }, + "supports_device_platform_breakdown": { + "type": "boolean", + "description": "Whether this product supports device platform breakdowns in delivery reporting (by_device_platform within by_package)" + }, + "supports_audience_breakdown": { + "type": "boolean", + "description": "Whether this product supports audience segment breakdowns in delivery reporting (by_audience within by_package)" + }, + "supports_placement_breakdown": { + "type": "boolean", + "description": "Whether this product supports placement breakdowns in delivery reporting (by_placement within by_package)" + }, + "date_range_support": { + "type": "string", + "enum": [ + "date_range", + "lifetime_only" + ], + "description": "Whether delivery data can be filtered to arbitrary date ranges. 'date_range' means the platform supports start_date/end_date parameters. 'lifetime_only' means the platform returns campaign lifetime totals and date range parameters are not accepted.", + "default": "date_range" + }, + "windowed_pull_granularities": { + "type": "array", + "description": "Granularities at which this product honors per-window pulls on get_media_buy_delivery (via request `time_granularity` + `include_window_breakdown: true`). Closes the GET-side half of the snapshot/log two-paths-parity contract for data-bearing events: a buyer who missed a webhook fire at any granularity listed here can reconstruct an identical payload by polling. Capability-scoped MUST \u2014 sellers MUST honor pulls at any granularity declared here, and MUST return UNSUPPORTED_GRANULARITY for pulls outside the set. Sellers MAY emit higher-frequency webhooks than they expose for pull (common where the webhook is a Kafka tap and historical reads go through a warehouse with coarser granularity); buyers see the gap up front via this capability and treat the webhook as primary for those frequencies. Absent or empty means the product only supports cumulative date-range pulls and full per-window recovery via GET is unavailable \u2014 see snapshot-and-log Rule 4.", + "items": { + "$ref": "#/$defs/ReportingFrequency" + }, + "uniqueItems": true, + "examples": [ + [ + "daily" + ], + [ + "hourly", + "daily" + ], + [ + "hourly", + "daily", + "monthly" + ] + ] + }, + "measurement_windows": { + "type": "array", + "description": "Measurement maturation stages available for this product. Used by any channel where billing-grade data is produced in phases rather than arriving final on day one. Examples: broadcast/linear TV (Live \u2192 C3 \u2192 C7 DVR accumulation), DOOH (tentative plays \u2192 post-IVT/fraud-check final), digital with IVT filtering (raw \u2192 GIVT filtered \u2192 SIVT filtered), podcast (7-day downloads \u2192 30-day downloads). Each window defines an accumulation period and expected data availability. When present, delivery reports reference a specific window_id. Sellers whose data is final on first delivery typically omit this.", + "items": { + "title": "Measurement Window", + "description": "A measurement maturation stage for any channel where billing-grade data is produced in phases rather than arriving final on day one. Each window represents an accumulation or processing stage with its own expected availability. Examples: broadcast/linear TV (live \u2192 C3 \u2192 C7 DVR accumulation), DOOH (tentative plays \u2192 post-IVT/fraud-check final), digital (raw impressions \u2192 GIVT filtered \u2192 SIVT filtered), podcast (7-day downloads \u2192 30-day downloads), audio/radio (tentative \u2192 diary/panel-certified). Sellers whose data is final on first delivery omit this.", + "type": "object", + "properties": { + "window_id": { + "type": "string", + "maxLength": 50, + "description": "Identifier for this maturation stage. Standard broadcast values: 'live' (real-time viewers only), 'c3' (live + 3 days time-shifted), 'c7' (live + 7 days time-shifted). Standard values for other channels include 'tentative' (provisional data available quickly), 'final' (post-processing certified data), 'post_ivt' (digital after invalid-traffic filtering), 'post_sivt' (digital after sophisticated-IVT filtering), 'downloads_7d' / 'downloads_30d' (podcast download maturation). Sellers may define custom IDs.", + "examples": [ + "live", + "c3", + "c7", + "tentative", + "final", + "post_ivt", + "downloads_30d" + ] + }, + "description": { + "type": "string", + "maxLength": 500, + "description": "Human-readable description of what this window measures", + "examples": [ + "Live broadcast impressions only", + "Live plus 7 days of time-shifted viewing", + "Tentative plays before IVT and fraud-check processing", + "Final plays after IVT and fraud-check processing", + "Impressions after sophisticated invalid-traffic filtering" + ] + }, + "duration_days": { + "type": "integer", + "description": "Number of days of accumulation included in this window before processing begins. For broadcast, this is DVR accumulation (0 = live only, 3 = live + 3 days DVR, 7 = live + 7 days DVR). For channels without an accumulation period (DOOH tentative\u2192final, digital IVT filtering), this is 0 \u2014 maturation is entirely vendor processing time captured in expected_availability_days.", + "minimum": 0 + }, + "expected_availability_days": { + "type": "integer", + "description": "Expected number of days after delivery before this window's data is available from the measurement vendor. Captures accumulation time plus vendor processing time. Examples: broadcast C7 from VideoAmp ~22 days (7-day accumulation + ~15-day processing); DOOH tentative plays same-day; DOOH final (post-IVT/fraud-check) ~1 day; digital post-SIVT ~2\u20133 days.", + "minimum": 0 + }, + "is_guarantee_basis": { + "type": "boolean", + "description": "Whether this window is the basis for delivery guarantees, reconciliation, and invoicing. A product typically has one guarantee basis window (e.g., C7 for most US broadcast, post-IVT final for DOOH). Buyers reconcile against the guarantee basis window's final numbers." + } + }, + "required": [ + "window_id", + "duration_days" + ], + "additionalProperties": true + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "available_reporting_frequencies", + "expected_delay_minutes", + "timezone", + "supports_webhooks", + "available_metrics", + "date_range_support" + ], + "additionalProperties": true + }, + "creative_policy": { + "title": "Creative Policy", + "description": "Creative requirements and restrictions for a product", + "type": "object", + "properties": { + "co_branding": { + "$ref": "#/$defs/CoBrandingRequirement" + }, + "landing_page": { + "$ref": "#/$defs/LandingPageRequirement" + }, + "templates_available": { + "type": "boolean", + "description": "Whether creative templates are provided" + }, + "provenance_required": { + "type": "boolean", + "description": "Whether creatives must include provenance metadata. When true, the seller requires buyers to attach provenance declarations to creative submissions. The seller may independently verify claims via get_creative_features." + }, + "provenance_requirements": { + "type": "object", + "description": "Structured provenance requirements for creatives. Refines `provenance_required`: when `provenance_required` is true, the fields in this object specify which provenance features the seller requires. When `provenance_required` is false or absent, this object SHOULD be absent; if present, receivers MUST ignore it. Existing seller agents that do not read this object are unaffected; the wire shape does not change for them. Sellers that publish a requirement here MUST enforce it on creative submission: a `sync_creatives` request that omits a required field is rejected with the corresponding `PROVENANCE_*` error code (see error-code.json), and a creative whose provenance claim is contradicted by an independent verification (`get_creative_features` against a governance agent the seller operates or has allowlisted via `accepted_verifiers`) is rejected with `PROVENANCE_CLAIM_CONTRADICTED`. This is the structural-rejection surface; the truth-of-claim surface lives in `get_creative_features`. Field-level requirements are seller-enforced \u2014 JSON Schema validation does not check them.", + "properties": { + "require_digital_source_type": { + "type": "boolean", + "description": "When true, the seller requires creatives to include a `digital_source_type` field in their provenance, set to a valid value from the `digital-source-type` enum (not null or absent). Submissions that omit this field are rejected with `PROVENANCE_DIGITAL_SOURCE_TYPE_MISSING`. Supports EU AI Act Art. 50 and CA SB 942 compliance workflows where AI disclosure metadata must be present at the protocol level." + }, + "require_disclosure_metadata": { + "type": "boolean", + "description": "When true, the seller requires creatives to include a `disclosure` object in their provenance with `disclosure.required` set to a boolean value (true or false). When `disclosure.required` is true, at least one entry in `disclosure.jurisdictions` is expected. Submissions that omit `disclosure.required` are rejected with `PROVENANCE_DISCLOSURE_MISSING`." + }, + "require_embedded_provenance": { + "type": "boolean", + "description": "When true, the seller requires creatives to include at least one `embedded_provenance` entry. For pipelines where sidecar metadata is stripped by intermediaries, this ensures provenance data persists through delivery. Submissions that omit `embedded_provenance` are rejected with `PROVENANCE_EMBEDDED_MISSING`." + } + }, + "additionalProperties": true + }, + "accepted_verifiers": { + "type": "array", + "description": "Governance agents the seller operates, has allowlisted, or otherwise trusts to verify provenance claims via `get_creative_features`. Buyers attaching a `verify_agent` pointer on `embedded_provenance[]` or `watermarks[]` MUST select an `agent_url` that appears in this list (canonicalized per /docs/reference/url-canonicalization: lowercase scheme and host, strip default port, normalize path dot-segments) - the buyer is *representing* that they used a verifier the seller will recognize, not asserting unilateral routing. Sellers MUST reject `sync_creatives` submissions whose `verify_agent.agent_url` does not match any entry here with `PROVENANCE_VERIFIER_NOT_ACCEPTED`. The seller is the verifier-of-record: it is the seller, not the buyer, that decides which agent it will call. Publishing the list lets buyers pre-flight their creative shape against `get_products` and lets multiple buyers converge on the same verifier without coordinating with each other.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "URL of the governance agent. MUST use the `https://` scheme. The seller calls this URL via `get_creative_features` to verify a buyer's claim; the seller has already vetted the endpoint and accepts responsibility for outbound calls to it." + }, + "feature_id": { + "type": "string", + "description": "Optional canonical `feature_id` the seller will request against this agent (e.g., `encypher.markers_present_v2`). When present, the buyer's `verify_agent.feature_id` SHOULD either match this value or be omitted. When absent, the seller selects a feature from the agent's `governance.creative_features` catalog at evaluation time. Resolves the selector ambiguity that would otherwise let two compliant receivers reach different verdicts." + }, + "providers": { + "type": "array", + "description": "Optional `provider` labels this agent verifies (e.g., `['Encypher', 'Digimarc']`). When present, sellers SHOULD only invoke this agent for `embedded_provenance[]` / `watermarks[]` entries whose `provider` field matches one of these labels \u2014 letting buyers pre-flight whether their attached evidence is verifiable against the seller's allowlist. When absent, the agent is treated as provider-agnostic.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + } + } + }, + "required": [ + "co_branding", + "landing_page", + "templates_available" + ], + "additionalProperties": true + }, + "is_custom": { + "type": "boolean", + "description": "Whether this is a custom product" + }, + "property_targeting_allowed": { + "type": "boolean", + "default": false, + "description": "Whether buyers can filter this product to a subset of its publisher_properties. When false (default), the product is 'all or nothing' - buyers must accept all properties or the product is excluded from property_list filtering results." + }, + "data_provider_signals": { + "type": "array", + "description": "Deprecated. Legacy/non-selectable metadata for data-provider catalog signals already bundled into or associated with this product. This field does not provide buyer-selectable options, prices, or seller activation handles. Use included_signals for non-selectable product signal metadata, or signal_targeting_options for selectable package-level signal groups.", + "deprecated": true, + "items": { + "$ref": "#/$defs/DataProviderSignalSelector" + } + }, + "included_signals": { + "type": "array", + "description": "Non-selectable signal metadata for signals already included in, bundled with, or planned into this product. These signals describe what the product is; buyers do not select them in packages[].targeting_overlay.signal_targeting_groups and this field does not imply package-level signal targeting. Use signal_ref scope 'data_provider' or 'signal_source' to reference externally defined signals without redefining their name or value_type. Use signal_ref scope 'product' with name and value_type when the included signal is defined only by this product.", + "items": { + "title": "Signal Listing", + "description": "Shared signal identity and definition metadata used when a signal is listed outside its authoritative catalog. New listings carry signal_ref; legacy listings may carry deprecated signal_id during the SignalRef migration window. Product-local signals use the listing as the definition boundary and MUST include name and value_type. Data-provider and signal-source refs MAY omit definition metadata when the buyer can resolve it from the referenced catalog or source; any supplied name, description, value_type, categories, range, methodology_url, or last_updated is product/account/source context and does not replace the authoritative definition.", + "type": "object", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined by a data provider's published adagents.json signal catalog; use scope 'signal_source' with signal_source_url for a source-native signal.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "name": { + "type": "string", + "description": "Human-readable signal name. Required when signal_ref.scope is 'product'. For data_provider and signal_source refs, this is optional contextual display text; the referenced catalog or source remains authoritative." + }, + "description": { + "type": "string", + "description": "Detailed signal description. For data_provider and signal_source refs, this is optional contextual display text and MUST NOT replace the referenced definition." + }, + "methodology_url": { + "type": "string", + "format": "uri", + "description": "Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition." + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "When this listing record was last updated. This indicates freshness of the listing record, not an attestation that the underlying data or model was refreshed at that time." + }, + "value_type": { + "$ref": "#/$defs/SignalValueType" + }, + "categories": { + "type": "array", + "description": "Valid values for categorical signals. Present when value_type is 'categorical'.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "range": { + "type": "object", + "description": "Valid range for numeric signals. Present when value_type is 'numeric'.", + "properties": { + "min": { + "type": "number", + "description": "Minimum value, inclusive." + }, + "max": { + "type": "number", + "description": "Maximum value, inclusive." + } + }, + "required": [ + "min", + "max" + ], + "additionalProperties": false + } + }, + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "allOf": [ + { + "description": "Product-local signal listings define the signal inline, so they need a display name and value type.", + "if": { + "properties": { + "signal_ref": { + "type": "object", + "properties": { + "scope": { + "const": "product" + } + }, + "required": [ + "scope" + ] + } + }, + "required": [ + "signal_ref" + ] + }, + "then": { + "required": [ + "name", + "value_type" + ] + } + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "signal_targeting_options": { + "type": "array", + "description": "Inline seller-offered signals that may be applied to packages for this product at create_media_buy time. Each entry references a named signal definition with signal_ref scope 'product' for a product-local signal option, scope 'data_provider' for an external published adagents.json signal catalog the seller is authorized to apply, or scope 'signal_source' for a source-native signal. Product-local options define name and value_type inline; data-provider and signal-source options may omit those fields when the referenced catalog or source is authoritative. Use this field when the selectable menu is product-specific, has product-specific pricing or activation handles, is the relevant subset for a brief/refine result, or should be rendered without an additional get_signals call. Wholesale products may omit this field and rely on get_signals for the selectable signal feed. Buyers select eligible signals through packages[].targeting_overlay.signal_targeting_groups when signal_targeting_rules allow; fixed/default entries are applied by the seller and echoed on the package state. Sellers MUST set signal_targeting_allowed to true whenever this field is present. Bundled, non-selectable signal metadata belongs in included_signals; legacy data_provider_signals may appear only for backwards compatibility.", + "items": { + "title": "Product Signal Targeting Option", + "description": "A signal the seller makes available inline for package-level signal composition on this product. Product.signal_targeting_options is used when the product needs product-scoped pricing, activation handles, defaults, grouping hints, a brief/refine-selected subset, or a curated inline menu. Wholesale products can instead omit inline options when the selectable menu is the broader get_signals feed. Product-local signals define their name and value_type inline through the shared signal-listing fields; data-provider and signal-source refs may omit those definition fields when the referenced catalog or source is authoritative.", + "type": "object", + "allOf": [ + { + "title": "Signal Listing", + "description": "Shared signal identity and definition metadata used when a signal is listed outside its authoritative catalog. New listings carry signal_ref; legacy listings may carry deprecated signal_id during the SignalRef migration window. Product-local signals use the listing as the definition boundary and MUST include name and value_type. Data-provider and signal-source refs MAY omit definition metadata when the buyer can resolve it from the referenced catalog or source; any supplied name, description, value_type, categories, range, methodology_url, or last_updated is product/account/source context and does not replace the authoritative definition.", + "type": "object", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Canonical signal reference. Use scope 'product' for a product-local signal defined by this listing; use scope 'data_provider' with data_provider_domain for a signal defined by a data provider's published adagents.json signal catalog; use scope 'signal_source' with signal_source_url for a source-native signal.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older Signals Protocol clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "name": { + "type": "string", + "description": "Human-readable signal name. Required when signal_ref.scope is 'product'. For data_provider and signal_source refs, this is optional contextual display text; the referenced catalog or source remains authoritative." + }, + "description": { + "type": "string", + "description": "Detailed signal description. For data_provider and signal_source refs, this is optional contextual display text and MUST NOT replace the referenced definition." + }, + "methodology_url": { + "type": "string", + "format": "uri", + "description": "Optional link to published methodology, media-kit, or data documentation. For data_provider and signal_source refs, this SHOULD match or supplement the referenced definition." + }, + "last_updated": { + "type": "string", + "format": "date-time", + "description": "When this listing record was last updated. This indicates freshness of the listing record, not an attestation that the underlying data or model was refreshed at that time." + }, + "value_type": { + "$ref": "#/$defs/SignalValueType" + }, + "categories": { + "type": "array", + "description": "Valid values for categorical signals. Present when value_type is 'categorical'.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "range": { + "type": "object", + "description": "Valid range for numeric signals. Present when value_type is 'numeric'.", + "properties": { + "min": { + "type": "number", + "description": "Minimum value, inclusive." + }, + "max": { + "type": "number", + "description": "Maximum value, inclusive." + } + }, + "required": [ + "min", + "max" + ], + "additionalProperties": false + } + }, + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "allOf": [ + { + "description": "Product-local signal listings define the signal inline, so they need a display name and value type.", + "if": { + "properties": { + "signal_ref": { + "type": "object", + "properties": { + "scope": { + "const": "product" + } + }, + "required": [ + "scope" + ] + } + }, + "required": [ + "signal_ref" + ] + }, + "then": { + "required": [ + "name", + "value_type" + ] + } + } + ], + "additionalProperties": true + }, + { + "description": "Signals that require activation need an operational handle the buyer can pass to activate_signal before package selection.", + "if": { + "properties": { + "activation_status": { + "const": "requires_activation" + } + }, + "required": [ + "activation_status" + ] + }, + "then": { + "required": [ + "signal_agent_segment_id" + ] + } + } + ], + "required": [ + "signal_ref" + ], + "properties": { + "signal_agent_segment_id": { + "type": "string", + "description": "Optional opaque seller execution handle for this signal. Omit when signal_ref is sufficient for the seller to resolve the signal. Include only when the seller exposes a distinct runtime or activation handle that buyers must echo in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].signal_agent_segment_id.", + "x-entity": "signal_activation_id" + }, + "activation_status": { + "type": "string", + "description": "Whether this signal option is ready to select on create_media_buy for the requesting account. 'ready' means the buyer can select it directly. 'requires_activation' means the buyer must activate the signal first or include an activation_key the seller accepts.", + "enum": [ + "ready", + "requires_activation" + ], + "default": "ready" + }, + "allowed_targeting_modes": { + "type": "array", + "description": "How this signal may be used when composing package-level signal targeting groups. 'include' means the signal may appear in an 'any' child group. 'exclude' means the signal may appear in a 'none' child group. Omit when the signal is include-only. This field declares the allowed buy-time group operator; binary package signal entries still use value=true in both include and exclude groups.", + "items": { + "type": "string", + "enum": [ + "include", + "exclude" + ] + }, + "uniqueItems": true, + "minItems": 1, + "default": [ + "include" + ] + }, + "default_selected": { + "type": "boolean", + "description": "Whether the seller recommends or preselects this signal when composing this product. Buyers may remove it unless signal_targeting_rules.selection_mode is 'fixed'. When selection_mode is 'fixed', sellers apply default_selected signals even if the buyer omits signal_targeting_groups and MUST echo the applied entries on the resulting package state.", + "default": false + }, + "selection_group": { + "type": "string", + "description": "Optional product-defined composability bucket for signal options, such as alternative audience tiers, a key-value targeting plane, or an audience-segment targeting plane. Signals in the same selection_group are expected to be OR-combinable inside one child group for a given targeting mode, subject to signal_targeting_rules. Use different selection_group values when the product requires separate ANDed clauses, such as signal sets backed by different platform targeting primitives that cannot be collapsed into one child group. selection_group is a product-option grouping key, not a reference to one child object in packages[].targeting_overlay.signal_targeting_groups.groups[]. Sellers can use signal_targeting_rules.max_selected_per_group and signal_targeting_rules.selection_group_rules with selection_group to guide and validate storefront composition." + }, + "pricing_options": { + "type": "array", + "description": "Signal pricing options available when this signal is selected on this product. Product-scoped pricing is authoritative for this product; if get_signals exposes a different default rate card, use this product-scoped price when composing the buy. Buyers pass the selected pricing_option_id in packages[].targeting_overlay.signal_targeting_groups.groups[].signals[].pricing_option_id. Omit when the signal is bundled into the product price or has no incremental cost.", + "items": { + "title": "Vendor Pricing Option", + "description": "A pricing option offered by a vendor agent (signals, creative, governance). Combines pricing_option_id with the pricing model fields. Pass pricing_option_id in report_usage for billing verification. All vendor discovery responses return pricing_options as an array \u2014 vendors may offer multiple options (volume tiers, context-specific rates, different models per product line).", + "allOf": [ + { + "type": "object", + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Opaque identifier for this pricing option, unique within the vendor agent. Pass this in report_usage to identify which pricing option was applied.", + "x-entity": "vendor_pricing_option" + } + }, + "required": [ + "pricing_option_id" + ] + }, + { + "title": "Vendor Pricing", + "description": "Pricing model for a vendor service. Discriminated by model: 'cpm' (fixed CPM), 'percent_of_media' (percentage of spend with optional CPM cap), 'flat_fee' (fixed charge per reporting period), 'per_unit' (fixed price per unit of work), or 'custom' (escape hatch for models not covered by the enumerated forms \u2014 requires a description and structured metadata).", + "type": "object", + "discriminator": { + "propertyName": "model" + }, + "oneOf": [ + { + "title": "CpmPricing", + "description": "Fixed cost per thousand impressions", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "cpm" + }, + "cpm": { + "type": "number", + "description": "Cost per thousand impressions", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "cpm", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PercentOfMediaPricing", + "description": "Percentage of media spend charged for this signal. When max_cpm is set, the effective rate is capped at that CPM \u2014 useful for platforms like The Trade Desk that use percent-of-media pricing with a CPM ceiling.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "percent_of_media" + }, + "percent": { + "type": "number", + "description": "Percentage of media spend, e.g. 15 = 15%", + "minimum": 0, + "maximum": 100 + }, + "max_cpm": { + "type": "number", + "description": "Optional CPM cap. When set, the effective charge is min(percent \u00d7 media_spend_per_mille, max_cpm).", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for the resulting charge", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "percent", + "currency" + ], + "additionalProperties": true + }, + { + "title": "FlatFeePricing", + "description": "Fixed charge per billing period, regardless of impressions or spend. Used for licensed data bundles and audience subscriptions.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "flat_fee" + }, + "amount": { + "type": "number", + "description": "Fixed charge for the billing period", + "minimum": 0 + }, + "period": { + "type": "string", + "enum": [ + "monthly", + "quarterly", + "annual", + "campaign" + ], + "description": "Billing period for the flat fee." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "amount", + "period", + "currency" + ], + "additionalProperties": true + }, + { + "title": "PerUnitPricing", + "description": "Fixed price per unit of work. Used for creative transformation (per format), AI generation (per image, per token), and rendering (per variant). The unit field describes what is counted; unit_price is the cost per one unit.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "per_unit" + }, + "unit": { + "type": "string", + "description": "What is counted \u2014 e.g. 'format', 'image', 'token', 'variant', 'render', 'evaluation'." + }, + "unit_price": { + "type": "number", + "description": "Cost per one unit", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "unit", + "unit_price", + "currency" + ], + "additionalProperties": true + }, + { + "title": "CustomPricing", + "description": "Escape hatch for pricing constructs that do not fit cpm, percent_of_media, flat_fee, or per_unit. Use when a vendor prices via performance kickers, tiered volume, hybrid formulas, outcome-sharing, or any other model the standard forms cannot express. Requires a human-readable description and a structured metadata object that captures the parameters a buyer needs to reason about the charge. Buyers SHOULD route custom pricing through operator review before commitment \u2014 automatic selection is not recommended.", + "type": "object", + "properties": { + "model": { + "type": "string", + "const": "custom" + }, + "description": { + "type": "string", + "description": "Human-readable description of the custom pricing model. Buyers display this to the operator when requesting approval.", + "minLength": 1 + }, + "metadata": { + "type": "object", + "description": "Structured parameters for the custom model. Keys follow lowercase_snake_case. Values may be primitives, arrays, or nested objects. Must be sufficient for a human to understand the pricing basis and for a downstream system to reconstruct the charge. Vendors SHOULD include a `summary_for_operator` string (one or two sentences, suitable for display in a buyer's operator-review UI) so reviewers across vendors see a consistent prompt. Required operator-review fields (approver role, dollar threshold for automatic approval, escalation contact) MAY be surfaced via additional keys the buyer's review surface recognizes.", + "additionalProperties": true, + "minProperties": 1, + "properties": { + "summary_for_operator": { + "type": "string", + "description": "One or two sentences describing the pricing construct in plain language, displayed to the buyer's operator when requesting approval. Should not repeat the top-level `description` verbatim \u2014 summarize the charge mechanic instead (e.g., 'Base $12 CPM plus $0.50 per qualifying post-view conversion, capped at $45 CPM').", + "minLength": 1 + } + } + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. Present when the pricing resolves to a monetary charge in a specific currency.", + "pattern": "^[A-Z]{3}$" + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "model", + "description", + "metadata" + ], + "additionalProperties": true + } + ] + } + ] + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "minItems": 1 + }, + "signal_targeting_rules": { + "title": "Signal Targeting Rules", + "description": "Composition rules for selecting signals on this product. The selectable signal menu may come from inline signal_targeting_options or from get_signals when a wholesale product omits inline options. This is product-scoped because products may be backed by different ad servers with different Boolean targeting support and group limits.", + "type": "object", + "properties": { + "resolution_model": { + "type": "string", + "description": "How selected signal_targeting_options are resolved against the product's inventory. 'direct_targeting' means selected signals are applied as targeting predicates to the package inventory. 'seller_planned' means selected signals are planning inputs that the seller resolves against product-specific inventory, timing, availability, reach, or pacing constraints; buyers SHOULD NOT attempt to decompose the signal selection into lower-level inventory or schedule decisions. Use 'seller_planned' for products such as linear broadcast schedules where the audience definition may be portable but the audience-to-avails plan is seller-resolved.", + "enum": [ + "direct_targeting", + "seller_planned" + ], + "default": "direct_targeting" + }, + "selection_mode": { + "type": "string", + "description": "Default selection behavior for selectable signals on this product. 'optional' means the buyer may select zero or more signals. 'required' means the buyer must select at least min_selected_signals, or 1 when min_selected_signals is omitted. 'fixed' means the seller applies the default_selected signals and the buyer cannot add or remove them; buyers SHOULD render those entries as read-only and sellers MUST echo them in package targeting_overlay.signal_targeting_groups. Use selection_group_rules for product-scoped products that need different behavior for different groups, such as fixed suppressions plus a required include tier.", + "enum": [ + "optional", + "required", + "fixed" + ], + "default": "optional" + }, + "min_selected_signals": { + "type": "integer", + "description": "Minimum number of signals the buyer must select when selection_mode is 'required'. If selection_mode is 'required' and this field is omitted, sellers MUST treat the minimum as 1. Defaults to 0 for optional selection.", + "minimum": 0 + }, + "max_selected_signals": { + "type": "integer", + "description": "Maximum number of signals the buyer may select for a package. Omit when there is no declared limit beyond the available options.", + "minimum": 1 + }, + "max_selected_per_group": { + "type": "integer", + "description": "Maximum number of signal_targeting_options the buyer may select from the same ProductSignalTargetingOption.selection_group. Use 1 for mutually exclusive alternatives within each option group. This limit applies to product option grouping, not to the number of child groups in packages[].targeting_overlay.signal_targeting_groups.", + "minimum": 1 + }, + "max_signal_targeting_groups": { + "type": "integer", + "description": "Maximum number of child groups allowed in packages[].targeting_overlay.signal_targeting_groups.groups. Omit when the seller has no declared limit beyond product terms.", + "minimum": 1 + }, + "max_signals_per_targeting_group": { + "type": "integer", + "description": "Maximum number of signals allowed in each packages[].targeting_overlay.signal_targeting_groups.groups[].signals array. Omit when the seller has no declared limit beyond product terms.", + "minimum": 1 + }, + "selection_group_rules": { + "type": "array", + "description": "Optional product-scoped overrides for specific ProductSignalTargetingOption.selection_group values. Use this when one product has mixed behavior, such as fixed seller-applied suppressions, a required pick-one include tier, optional buyer-selected exclusions, or heterogeneous targeting planes that must be represented as separate ANDed clauses. Rules apply only to options whose selection_group matches. When selection_group_rules are present, each packages[].targeting_overlay.signal_targeting_groups child group MUST contain signals from exactly one selection_group and one targeting_mode, and buyers MUST send at most one child group for each (selection_group, targeting_mode) pair. Sellers MUST reject duplicate, mixed, or collapsed groups that combine distinct selection_group_rules into the same child group.", + "items": { + "type": "object", + "properties": { + "selection_group": { + "type": "string", + "description": "ProductSignalTargetingOption.selection_group value this rule applies to." + }, + "targeting_mode": { + "type": "string", + "description": "How options in this selection_group are intended to be used in signal_targeting_groups. 'include' maps to child groups with operator 'any'. 'exclude' maps to child groups with operator 'none'. Omit when options in the group may be used according to each option's allowed_targeting_modes.", + "enum": [ + "include", + "exclude" + ] + }, + "selection_mode": { + "type": "string", + "description": "Selection behavior for this selection_group. 'required' means at least min_selected_signals, or 1 when omitted. 'fixed' means default_selected options in this group are seller-applied and read-only.", + "enum": [ + "optional", + "required", + "fixed" + ] + }, + "min_selected_signals": { + "type": "integer", + "description": "Minimum selected options from this selection_group. If selection_mode is 'required' and omitted, sellers MUST treat the minimum as 1.", + "minimum": 0 + }, + "max_selected_signals": { + "type": "integer", + "description": "Maximum selected options from this selection_group.", + "minimum": 1 + } + }, + "required": [ + "selection_group" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "signal_targeting_allowed": { + "type": "boolean", + "default": false, + "description": "Whether this product has a package-level signal_targeting_groups surface. When false (default), signals are bundled into the product terms and cannot be selected or explicitly echoed as package signal groups. When true, eligible signals from inline signal_targeting_options or from get_signals may be buyer-selected or seller-applied according to signal_targeting_rules and are represented through packages[].targeting_overlay.signal_targeting_groups. Editability is controlled by signal_targeting_rules; fixed/default-only products still set this to true when applied signal groups are echoed." + }, + "catalog_types": { + "type": "array", + "description": "Catalog types this product supports for catalog-driven campaigns. A sponsored product listing declares [\"product\"], a job board declares [\"job\", \"offering\"]. Buyers match synced catalogs to products via this field.", + "items": { + "$ref": "#/$defs/CatalogType" + }, + "uniqueItems": true, + "minItems": 1 + }, + "metric_optimization": { + "type": "object", + "description": "Metric optimization capabilities for this product. Presence indicates the product supports optimization_goals with kind: 'metric'. No event source or conversion tracking setup required \u2014 the seller tracks these metrics natively.", + "properties": { + "supported_metrics": { + "type": "array", + "description": "Metric kinds this product can optimize for. Buyers should only request metric goals for kinds listed here. **DEPRECATED values** (slated for removal at next major): `attention_seconds` and `attention_score` \u2014 declare vendor-attested attention/quality metrics via `vendor_metric_optimization.supported_metrics[]` with an explicit vendor binding instead. Sellers MAY reject the deprecated values with `TERMS_REJECTED` and a suggestion to use the `vendor_metric` kind.", + "items": { + "type": "string", + "enum": [ + "clicks", + "views", + "completed_views", + "viewed_seconds", + "attention_seconds", + "attention_score", + "engagements", + "follows", + "saves", + "profile_visits", + "reach" + ] + }, + "minItems": 1 + }, + "supported_reach_units": { + "type": "array", + "description": "Reach units this product can optimize for. Required when supported_metrics includes 'reach'. Buyers must set reach_unit to a value in this list on reach optimization goals \u2014 sellers reject unsupported values.", + "items": { + "$ref": "#/$defs/ReachUnit" + }, + "minItems": 1 + }, + "supported_view_durations": { + "type": "array", + "description": "Video view duration thresholds (in seconds) this product supports for completed_views goals. Only relevant when supported_metrics includes 'completed_views'. When absent, the seller uses their platform default. Buyers must set view_duration_seconds to a value in this list \u2014 sellers reject unsupported values.", + "items": { + "type": "number", + "exclusiveMinimum": 0 + } + }, + "supported_targets": { + "type": "array", + "description": "Target kinds available for metric goals on this product. Values match target.kind on the optimization goal. Only these target kinds are accepted \u2014 goals with unlisted target kinds will be rejected. When omitted, buyers can set target-less metric goals (maximize volume within budget) but cannot set specific targets.", + "items": { + "type": "string", + "enum": [ + "cost_per", + "threshold_rate" + ] + } + } + }, + "required": [ + "supported_metrics" + ], + "additionalProperties": true + }, + "vendor_metric_optimization": { + "title": "Vendor Metric Optimization", + "description": "Vendor-attested metric optimization capabilities for this product. Presence indicates the product supports `optimization_goals` with `kind: 'vendor_metric'` \u2014 the seller's bidding stack can steer delivery toward a specific vendor's measurement (e.g., DV/IAS/Adelaide attention, Scope3 emissions, Kantar brand lift, retail-media partner metrics). Distinct from `metric_optimization` (seller-native metrics with no vendor binding) and from `reporting_capabilities.vendor_metrics` (which declares what the product can *report* rather than what it can *optimize against*). A product may report a vendor metric without being able to optimize for it. Buyers MUST verify the goal's `(vendor, metric_id)` is in `supported_metrics` AND that the package's `committed_metrics[]` includes a matching `{ scope: 'vendor', vendor, metric_id }` entry \u2014 optimization without committed reporting is unverifiable and is rejected at the wire level.", + "type": "object", + "properties": { + "supported_metrics": { + "type": "array", + "description": "Vendor-defined metrics this product can steer delivery toward. Each entry pairs a vendor identity (BrandRef anchored on the vendor's `brand.json` `agents[type='measurement']`) with a `metric_id` from that vendor's published `measurement.metrics[]` catalog, plus the target kinds the seller supports for the pair. Semantic uniqueness key is `(vendor.domain, vendor.brand_id, metric_id)`; sellers MUST de-duplicate before publication. JSON Schema `uniqueItems` blocks exact-object duplicates; semantic deduplication on the BrandRef-domain key is a seller obligation.", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. The vendor's `brand.json` is the discovery anchor for the measurement agent (entry with `type: 'measurement'` in the `agents[]` array); the metric's definition, methodology, and unit live at that agent's `get_adcp_capabilities.measurement.metrics[]` and are not duplicated inline here. Same shape as the `vendor` field on `reporting_capabilities.vendor_metrics` for symmetry across optimization and reporting capability declarations.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary (e.g., `attention_score`, `attention_seconds`, `gco2e_per_impression`, `awareness_lift`). MUST be present in the vendor's published `measurement.metrics[]` catalog.", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + }, + "supported_targets": { + "type": "array", + "description": "Target kinds available for `vendor_metric` goals against this `(vendor, metric_id)` pair. Values match `target.kind` on the optimization goal. `cost_per` \u2014 target cost per metric unit (e.g., $0.05 per attention-second). `threshold_rate` \u2014 minimum per-impression value (e.g., attention_score \u2265 70). Only these target kinds are accepted \u2014 goals with unlisted target kinds will be rejected. A goal without a target implicitly maximizes the metric within budget \u2014 no declaration needed for that mode. When omitted, buyers can still set target-less vendor_metric goals.", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "cost_per", + "threshold_rate" + ] + } + } + }, + "required": [ + "vendor", + "metric_id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "supported_metrics" + ], + "additionalProperties": true + }, + "max_optimization_goals": { + "type": "integer", + "minimum": 1, + "description": "Maximum number of optimization_goals this product accepts on a package. When absent, no limit is declared. Most social platforms accept only 1 goal \u2014 buyers sending arrays longer than this value should expect the seller to use only the highest-priority (lowest priority number) goal." + }, + "measurement_readiness": { + "title": "Measurement Readiness", + "description": "Assessment of whether the buyer's event source setup is sufficient for this product to optimize effectively. Only present when the seller can evaluate the buyer's account context. Buyers should check this before creating media buys with event-based optimization goals.", + "type": "object", + "properties": { + "status": { + "$ref": "#/$defs/AssessmentStatus" + }, + "required_event_types": { + "type": "array", + "description": "Event types this product needs for effective optimization. Buyers should ensure their event sources cover these types.", + "items": { + "$ref": "#/$defs/EventType" + }, + "minItems": 1 + }, + "missing_event_types": { + "type": "array", + "description": "Event types this product requires that the buyer has not configured. Empty or absent when all required types are covered.", + "items": { + "$ref": "#/$defs/EventType" + } + }, + "issues": { + "type": "array", + "description": "Actionable issues preventing full measurement readiness. Sellers should limit to the top 3-5 most actionable items. Buyer agents should sort by severity rather than relying on array position.", + "items": { + "title": "Diagnostic Issue", + "description": "An actionable issue detected during a health or readiness assessment. Used by event source health and measurement readiness to surface problems and recommendations.", + "type": "object", + "properties": { + "severity": { + "type": "string", + "enum": [ + "error", + "warning", + "info" + ], + "description": "'error': blocks optimization until resolved. 'warning': optimization works but effectiveness is reduced. 'info': suggestion for improvement." + }, + "message": { + "type": "string", + "description": "Human/agent-readable description of the issue and how to resolve it." + } + }, + "required": [ + "severity", + "message" + ], + "additionalProperties": true + } + }, + "notes": { + "type": "string", + "description": "Seller explanation of the readiness assessment, recommendations for improvement, or context about what the buyer needs to change." + } + }, + "required": [ + "status" + ], + "additionalProperties": true + }, + "conversion_tracking": { + "type": "object", + "description": "Conversion event tracking for this product. Presence indicates the product supports optimization_goals with kind: 'event'. Seller-level capabilities (supported event types, UID types, attribution windows) are declared in get_adcp_capabilities.", + "properties": { + "action_sources": { + "type": "array", + "description": "Action sources relevant to this product (e.g. a retail media product might have 'in_store' and 'website', while a display product might only have 'website')", + "items": { + "$ref": "#/$defs/ActionSource" + }, + "minItems": 1 + }, + "supported_targets": { + "type": "array", + "description": "Target kinds available for event goals on this product. Values match target.kind on the optimization goal. cost_per: target cost per conversion event. per_ad_spend: target return on ad spend (requires value_field on event sources). maximize_value: maximize total conversion value without a specific ratio target (requires value_field). Only these target kinds are accepted \u2014 goals with unlisted target kinds will be rejected. A goal without a target implicitly maximizes conversion count within budget \u2014 no declaration needed for that mode. When omitted, buyers can still set target-less event goals.", + "items": { + "type": "string", + "enum": [ + "cost_per", + "per_ad_spend", + "maximize_value" + ] + }, + "minItems": 1 + }, + "platform_managed": { + "type": "boolean", + "description": "Whether the seller provides its own always-on measurement (e.g. Amazon sales attribution for Amazon advertisers). When true, sync_event_sources response will include seller-managed event sources with managed_by='seller'." + } + }, + "additionalProperties": true + }, + "catalog_match": { + "type": "object", + "description": "When the buyer provides a catalog on get_products, indicates which catalog items are eligible for this product. Only present for products where catalog matching is relevant (e.g., sponsored product listings, job boards, hotel ads).", + "properties": { + "matched_gtins": { + "type": "array", + "description": "GTINs from the buyer's catalog that are eligible on this product's inventory. Standard GTIN formats (GTIN-8 through GTIN-14). Only present for product-type catalogs with GTIN matching.", + "items": { + "type": "string", + "pattern": "^[0-9]{8,14}$" + } + }, + "matched_ids": { + "type": "array", + "description": "Item IDs from the buyer's catalog that matched this product's inventory. The ID type depends on the catalog type and content_id_type (e.g., SKUs for product catalogs, job_ids for job catalogs, offering_ids for offering catalogs).", + "items": { + "type": "string" + } + }, + "matched_count": { + "type": "integer", + "description": "Number of catalog items that matched this product's inventory.", + "minimum": 0 + }, + "submitted_count": { + "type": "integer", + "description": "Total catalog items evaluated from the buyer's catalog.", + "minimum": 0 + } + }, + "required": [ + "submitted_count" + ] + }, + "brief_relevance": { + "type": "string", + "description": "Explanation of why this product matches the brief (only included when brief is provided)" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "Expiration timestamp. After this time, the product may no longer be available for purchase and create_media_buy may reject packages referencing it." + }, + "product_card": { + "type": "object", + "description": "Optional standard visual card for displaying this product in user interfaces (catalog browsers, dashboards, agent UIs). Distinct from `format` \u2014 product_card describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection. Receivers render the card directly from these fields.", + "properties": { + "image": { + "title": "Image Asset", + "description": "Hero image for the card. Recommended ~300x400 (4:3 portrait) for the standard card layout; receivers may scale.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "title": { + "type": "string", + "description": "Card title (typically the product name).", + "maxLength": 60 + }, + "description": { + "type": "string", + "description": "Short descriptive blurb shown below the title.", + "maxLength": 200 + }, + "price_label": { + "type": "string", + "description": "Formatted price or pricing summary (e.g., 'From $5 CPM', 'Auction floor $0.50 CPC'). Free-text \u2014 receivers render verbatim.", + "maxLength": 30 + }, + "cta_label": { + "type": "string", + "description": "Call-to-action button label (e.g., 'View details', 'Get proposal').", + "maxLength": 25 + } + }, + "additionalProperties": true + }, + "product_card_detailed": { + "type": "object", + "description": "Optional detailed card with hero + carousel + structured specifications, for rich product presentation (media-kit-style pages, full product detail views). Distinct from `format` \u2014 describes the UI rendering of the product itself, not the ad creative the product accepts. Typed inline; no format_id indirection.", + "properties": { + "hero_image": { + "title": "Image Asset", + "description": "Primary hero image at the top of the detailed view.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "carousel_images": { + "type": "array", + "description": "Additional images for a swipeable carousel below the hero.", + "items": { + "title": "Image Asset", + "description": "Image asset with URL and dimensions", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + } + }, + "title": { + "type": "string", + "description": "Page title (typically the product name)." + }, + "description": { + "type": "string", + "description": "Full descriptive copy. Markdown allowed in client renderers that support it; otherwise treat as plain text." + }, + "specifications": { + "type": "array", + "description": "Structured key/value specifications (e.g., 'Aspect ratio: 9:16', 'Duration: 30s'). Each item is a labeled fact about the product.", + "items": { + "type": "object", + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string", + "maxLength": 60 + }, + "value": { + "type": "string", + "maxLength": 200 + } + }, + "additionalProperties": true + } + }, + "price_label": { + "type": "string", + "description": "Formatted price or pricing summary." + }, + "cta_label": { + "type": "string", + "description": "Call-to-action button label." + } + }, + "additionalProperties": true + }, + "collections": { + "type": "array", + "description": "Collections available in this product. Each entry references collections declared in an adagents.json by domain and collection ID. Buyers resolve full collection objects from the referenced adagents.json.", + "items": { + "title": "Collection Selector", + "description": "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.", + "type": "object", + "properties": { + "publisher_domain": { + "type": "string", + "description": "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.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "collection_ids": { + "type": "array", + "description": "Collection IDs from the adagents.json collections array. Each ID must match a collection_id declared in that file.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "publisher_domain", + "collection_ids" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "collection_targeting_allowed": { + "type": "boolean", + "default": false, + "description": "Whether buyers can target a subset of this product's collections. When false (default), the product is a bundle \u2014 buyers get all listed collections. When true, buyers can select specific collections in the media buy." + }, + "installments": { + "type": "array", + "description": "Specific installments included in this product. Each installment references its parent collection via collection_id when the product spans multiple collections. When absent with collections present, the product covers the collections broadly (run-of-collection).", + "items": { + "title": "Installment", + "description": "A single bookable unit within a collection \u2014 one episode, issue, event, or rotation period. The parent collection's kind indicates how to interpret each installment: TV/podcast episodes, print issues, live event airings, newsletter editions, or DOOH rotation periods. Installments inherit collection-level fields they don't override: content_rating defaults to the collection's baseline, guest_talent is additive to the collection's recurring talent, and topics add context beyond the collection's genre.", + "type": "object", + "properties": { + "installment_id": { + "type": "string", + "description": "Unique identifier for this installment within the collection" + }, + "collection_id": { + "type": "string", + "description": "Parent collection reference. Required when the product spans multiple collections. Maps to a collection_id declared in one of the publishers' adagents.json files referenced by the product's collection selectors." + }, + "name": { + "type": "string", + "description": "Installment title" + }, + "season": { + "type": "string", + "description": "Season identifier (e.g., '1', '2024', 'spring_2026')" + }, + "installment_number": { + "type": "string", + "description": "Installment number within the season (e.g., '3', '47')" + }, + "scheduled_at": { + "type": "string", + "format": "date-time", + "description": "When the installment airs or publishes (ISO 8601)" + }, + "status": { + "$ref": "#/$defs/InstallmentStatus" + }, + "duration_seconds": { + "type": "integer", + "minimum": 0, + "description": "Expected duration of the installment in seconds" + }, + "flexible_end": { + "type": "boolean", + "description": "Whether the end time is approximate (live events, sports)" + }, + "valid_until": { + "type": "string", + "format": "date-time", + "description": "When this installment data expires and should be re-queried. Agents should re-query before committing budget to products with tentative installments." + }, + "content_rating": { + "title": "Content Rating", + "description": "Installment-specific content rating. Overrides the collection's baseline content_rating when present.", + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/ContentRatingSystem" + }, + "rating": { + "type": "string", + "description": "Rating value within the system (e.g., 'TV-PG', 'R', 'explicit')" + } + }, + "required": [ + "system", + "rating" + ], + "additionalProperties": true + }, + "topics": { + "type": "array", + "description": "Content topics for this installment. Uses the same taxonomy as the collection's genre_taxonomy when present. Enables installment-level brand safety evaluation beyond content_rating.", + "items": { + "type": "string" + } + }, + "special": { + "title": "Special", + "description": "Installment-specific event context. When present, this installment is anchored to a real-world event. Overrides the collection-level special when present.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the event (e.g., 'Olympics 2028', 'Super Bowl LXI')" + }, + "category": { + "$ref": "#/$defs/SpecialCategory" + }, + "starts": { + "type": "string", + "format": "date-time", + "description": "When the event starts (ISO 8601)" + }, + "ends": { + "type": "string", + "format": "date-time", + "description": "When the event ends (ISO 8601). Omit for single-day events." + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "guest_talent": { + "type": "array", + "description": "Installment-specific guests and talent. Additive to the collection's recurring talent.", + "items": { + "title": "Talent", + "description": "A person associated with a collection or installment, with an optional link to their brand.json identity", + "type": "object", + "properties": { + "role": { + "$ref": "#/$defs/TalentRole" + }, + "name": { + "type": "string", + "description": "Person's name as credited on the collection" + }, + "brand_url": { + "type": "string", + "format": "uri", + "description": "URL to this person's brand.json entry. Enables buyer agents to evaluate the talent's brand identity and associations." + } + }, + "required": [ + "role", + "name" + ], + "additionalProperties": true + } + }, + "ad_inventory": { + "title": "Ad Inventory Configuration", + "description": "Break-based ad inventory for this installment. For non-break formats (host reads, integrations), use product placements.", + "type": "object", + "properties": { + "expected_breaks": { + "type": "integer", + "minimum": 0, + "description": "Number of planned ad breaks in the installment" + }, + "total_ad_seconds": { + "type": "integer", + "minimum": 0, + "description": "Total seconds of ad time across all breaks" + }, + "max_ad_duration_seconds": { + "type": "integer", + "minimum": 1, + "description": "Maximum duration in seconds for a single ad within a break. Buyers need this to know whether their creative fits." + }, + "unplanned_breaks": { + "type": "boolean", + "description": "Whether ad breaks are dynamic and driven by live conditions (sports timeouts, election coverage). When false, all breaks are pre-defined." + }, + "supported_formats": { + "type": "array", + "description": "Ad format types supported in breaks (e.g., 'video', 'audio', 'display')", + "items": { + "type": "string" + } + } + }, + "required": [ + "expected_breaks" + ], + "additionalProperties": true + }, + "deadlines": { + "title": "Installment Deadlines", + "description": "Booking, cancellation, and material submission deadlines for this installment. Present when the installment has time-sensitive inventory that requires advance commitment or material delivery.", + "type": "object", + "properties": { + "booking_deadline": { + "type": "string", + "format": "date-time", + "description": "Last date/time to book a placement in this installment (ISO 8601). After this point, the seller will not accept new bookings." + }, + "cancellation_deadline": { + "type": "string", + "format": "date-time", + "description": "Last date/time to cancel without penalty (ISO 8601). Cancellations after this point may incur fees per the seller's terms." + }, + "material_deadlines": { + "type": "array", + "description": "Stages for creative material submission. Items MUST be in chronological order by due_at (earliest first). Typical pattern: 'draft' for raw materials the seller will process, 'final' for production-ready assets. Print example: draft artwork then press-ready PDF. Influencer example: talking points then approved script.", + "items": { + "title": "Material Deadline", + "description": "A deadline for creative material submission. Sellers declare stages to distinguish draft materials (e.g., talking points, raw artwork) from production-ready assets (e.g., approved scripts, press-ready PDFs).", + "type": "object", + "properties": { + "stage": { + "type": "string", + "description": "Submission stage identifier. Use 'draft' for materials that need seller processing and 'final' for production-ready assets. Sellers may define additional stages.", + "examples": [ + "draft", + "final" + ] + }, + "due_at": { + "type": "string", + "format": "date-time", + "description": "When materials for this stage are due (ISO 8601)" + }, + "label": { + "type": "string", + "description": "What the seller needs at this stage (e.g., 'Talking points and brand guidelines', 'Press-ready PDF with bleed')" + } + }, + "required": [ + "stage", + "due_at" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "minProperties": 1, + "additionalProperties": true + }, + "derivative_of": { + "type": "object", + "description": "When this installment is a clip, highlight, or recap derived from a full installment. The source installment_id must reference an installment within the same response.", + "properties": { + "installment_id": { + "type": "string", + "description": "The source installment this content is derived from" + }, + "type": { + "$ref": "#/$defs/DerivativeType" + } + }, + "required": [ + "installment_id", + "type" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "installment_id" + ], + "additionalProperties": true + } + }, + "enforced_policies": { + "type": "array", + "description": "Registry policy IDs the seller enforces for this product. Enforcement level comes from the policy registry. Buyers can filter products by required policies.", + "items": { + "type": "string" + } + }, + "trusted_match": { + "type": "object", + "description": "Trusted Match Protocol capabilities for this product. When present, the product supports real-time contextual and/or identity matching via TMP. Buyers use this to determine what response types the publisher can accept and whether brands can be selected dynamically at match time.", + "properties": { + "context_match": { + "type": "boolean", + "description": "Whether this product supports Context Match requests. When true, the publisher's TMP router will send context match requests to registered providers for this product's inventory.", + "default": true + }, + "identity_match": { + "type": "boolean", + "description": "Whether this product supports Identity Match requests. When true, the publisher's TMP router will send identity match requests to evaluate user eligibility.", + "default": false + }, + "response_types": { + "type": "array", + "description": "What the publisher can accept back from context match.", + "items": { + "$ref": "#/$defs/TMPResponseType" + }, + "minItems": 1, + "default": [ + "activation" + ] + }, + "dynamic_brands": { + "type": "boolean", + "description": "Whether the buyer can select a brand at match time. When false (default), the brand must be specified on the media buy/package. When true, the buyer's offer can include any brand \u2014 the publisher applies approval rules at match time. Enables multi-brand agreements where the holding company or buyer agent selects brand based on context.", + "default": false + }, + "providers": { + "type": "array", + "description": "TMP providers integrated with this product's inventory. Each entry identifies a provider by agent_url (from the registry) and declares what match types it supports for this product. The product-level context_match and identity_match booleans declare what the product supports overall; the per-provider booleans declare which provider handles each match type. Enables buyer discovery: 'find products where a specific provider does context matching.'", + "items": { + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "Provider's agent URL from the registry. Canonical identifier for this TMP provider." + }, + "context_match": { + "type": "boolean", + "description": "Whether this provider handles context match for this product.", + "default": false + }, + "identity_match": { + "type": "boolean", + "description": "Whether this provider handles identity match for this product.", + "default": false + }, + "countries": { + "type": "array", + "description": "ISO 3166-1 alpha-2 country codes this provider serves for identity match. The router uses this to select the correct regional provider based on the request's country field. Required when identity_match is true.", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "uid_types": { + "type": "array", + "description": "Identity types this regional provider can resolve. The router filters providers whose uid_types includes the request's uid_type. Required when identity_match is true.", + "items": { + "$ref": "#/$defs/UIDType" + }, + "minItems": 1 + } + }, + "required": [ + "agent_url" + ], + "if": { + "properties": { + "identity_match": { + "const": true + } + }, + "required": [ + "identity_match" + ] + }, + "then": { + "required": [ + "agent_url", + "countries", + "uid_types" + ] + }, + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "context_match" + ], + "additionalProperties": true + }, + "material_submission": { + "type": "object", + "description": "Instructions for submitting physical creative materials (print, static OOH, cinema). Present only for products requiring physical delivery outside the digital creative assignment flow. Buyer agents MUST validate url and email domains against the seller's known domains (from adagents.json) before submitting materials. Never auto-submit without human confirmation.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL for uploading or submitting physical creative materials" + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address for creative material submission" + }, + "instructions": { + "type": "string", + "description": "Human-readable instructions for material submission (file naming conventions, shipping address, etc.)", + "maxLength": 2000 + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "minProperties": 1, + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "product_id", + "name", + "description", + "publisher_properties", + "delivery_type", + "pricing_options", + "reporting_capabilities" + ], + "allOf": [ + { + "description": "Products with package-level signal targeting options or rules MUST declare the signal targeting surface explicitly.", + "if": { + "anyOf": [ + { + "required": [ + "signal_targeting_options" + ] + }, + { + "required": [ + "signal_targeting_rules" + ] + } + ] + }, + "then": { + "properties": { + "signal_targeting_allowed": { + "const": true + } + }, + "required": [ + "signal_targeting_allowed" + ] + } + } + ], + "anyOf": [ + { + "title": "Legacy Product (named-format reference)", + "description": "Product references one or more named formats by structured format_id ({ agent_url, id }). This is the legacy named-format path; it remains supported through 4.x.", + "required": [ + "format_ids" + ] + }, + { + "title": "3.1+ Product (format-option declarations)", + "description": "Product carries one or more inline ProductFormatDeclarations, each narrowing a canonical format. This is the 3.1+ format-option path introduced by RFC #3305. A single-element `format_options` array is the 90% case; multi-element arrays declare that the product accepts any of the listed format options.", + "required": [ + "format_options" + ] + } + ], + "additionalProperties": true + } + }, + "suggestions": { + "type": "array", + "description": "Suggested values or options for the required input", + "items": { + "type": "string" + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + { + "title": "Get Products - Submitted", + "description": "Async task envelope returned when get_products cannot be confirmed before the response \u2014 for example, when custom or bespoke product curation is queued for processing. The buyer polls tasks/get with task_id or receives a webhook when the task completes; the products array lands on the completion artifact, not this envelope.", + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "submitted", + "description": "Task-level status literal. Discriminates this async envelope from the synchronous success shape, whose products array is issued in-line. See task-status.json for the full task-status enum." + }, + "task_id": { + "type": "string", + "description": "Task handle the buyer uses with tasks/get, and that the seller references on push-notification callbacks. The products array is issued on the completion artifact, not here. Per AdCP wire conventions this is snake_case; A2A adapters MAY surface it as taskId, but the payload field emitted by the agent is task_id.", + "x-entity": "task" + }, + "message": { + "type": "string", + "maxLength": 2000, + "description": "Optional human-readable explanation of why the task is submitted \u2014 e.g., 'Custom curation queued; typical turnaround 10\u201330 minutes.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context \u2014 a hostile seller may inject prompt-injection payloads aimed at the buyer's agent." + }, + "estimated_completion": { + "type": "string", + "format": "date-time", + "description": "Estimated completion time for the search" + }, + "errors": { + "type": "array", + "description": "Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "status", + "task_id" + ], + "additionalProperties": true + }, + { + "title": "Create Media Buy Response", + "description": "Response payload for create_media_buy. Exactly one of three shapes: (1) synchronous success \u2014 media_buy_id and packages are issued in-line, no status or a MediaBuyStatus value (pending_creatives / pending_start / active); (2) terminal failure \u2014 an errors array with no media-buy artifact and status != 'submitted'; (3) submitted task envelope \u2014 status 'submitted' with task_id, the media buy is queued or awaiting a human decision (e.g., IO signing), and media_buy_id / packages land on the task's completion artifact, not this response. The submitted branch MAY carry advisory errors for non-blocking warnings; terminal failures belong in the error branch. These three shapes are mutually exclusive \u2014 a response has exactly one.", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "$ref": "#/$defs/AuthenticationScheme" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "title": "CreateMediaBuySuccess", + "description": "Success response - media buy created successfully", + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Seller's unique identifier for the created media buy", + "x-entity": "media_buy" + }, + "account": { + "title": "Account", + "description": "Account billed for this media buy. Includes advertiser, billing proxy (if any), and rate card applied.", + "type": "object", + "properties": { + "account_id": { + "type": "string", + "description": "Unique identifier for this account", + "x-entity": "account" + }, + "name": { + "type": "string", + "description": "Human-readable account name (e.g., 'Acme', 'Acme c/o Pinnacle')" + }, + "advertiser": { + "type": "string", + "description": "The advertiser whose rates apply to this account" + }, + "billing_proxy": { + "type": "string", + "description": "Optional intermediary who receives invoices on behalf of the advertiser (e.g., agency)" + }, + "status": { + "$ref": "#/$defs/AccountStatus" + }, + "brand": { + "title": "Brand Reference", + "description": "Brand reference identifying the advertiser", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "operator": { + "type": "string", + "description": "Domain of the entity operating this account. When the brand operates directly, this is the brand's domain.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$", + "x-entity": "operator" + }, + "billing": { + "$ref": "#/$defs/BillingParty" + }, + "billing_entity": { + "title": "Business Entity", + "description": "Business entity details for the party responsible for payment. Contains the legal name, tax IDs, address, and bank details needed for formal B2B invoicing. Corresponds to whoever billing points to (operator, agent, or advertiser). When this account appears in a response, bank details MUST be omitted (write-only).", + "type": "object", + "properties": { + "legal_name": { + "type": "string", + "description": "Registered legal name of the business entity", + "maxLength": 200 + }, + "vat_id": { + "type": "string", + "description": "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.", + "pattern": "^[A-Z]{2}[A-Z0-9]{2,13}$" + }, + "tax_id": { + "type": "string", + "description": "Tax identification number for jurisdictions that do not use VAT (e.g., US EIN)", + "maxLength": 30 + }, + "registration_number": { + "type": "string", + "description": "Company registration number (e.g., HRB 12345 for German Handelsregister)", + "maxLength": 50 + }, + "address": { + "type": "object", + "description": "Postal address for invoicing and legal correspondence", + "properties": { + "street": { + "type": "string", + "description": "Street address including building number", + "maxLength": 200 + }, + "city": { + "type": "string", + "maxLength": 100 + }, + "postal_code": { + "type": "string", + "maxLength": 20 + }, + "region": { + "type": "string", + "description": "State, province, or region", + "maxLength": 100 + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code", + "pattern": "^[A-Z]{2}$" + } + }, + "required": [ + "street", + "city", + "postal_code", + "country" + ], + "additionalProperties": false + }, + "contacts": { + "type": "array", + "description": "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.", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "billing", + "legal", + "creative", + "general" + ], + "enumDescriptions": { + "billing": "Accounts payable and invoice queries", + "legal": "Contract and compliance matters", + "creative": "Material submission and creative approval", + "general": "Default contact when no specific role applies" + }, + "description": "Contact's functional role in the business relationship" + }, + "name": { + "type": "string", + "description": "Full name of the contact", + "maxLength": 200 + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 254 + }, + "phone": { + "type": "string", + "maxLength": 30 + } + }, + "required": [ + "role" + ], + "additionalProperties": false + }, + "maxItems": 10 + }, + "bank": { + "type": "object", + "writeOnly": true, + "description": "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.", + "properties": { + "account_holder": { + "type": "string", + "description": "Name on the bank account", + "maxLength": 200 + }, + "iban": { + "type": "string", + "description": "International Bank Account Number (SEPA markets)", + "pattern": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$" + }, + "bic": { + "type": "string", + "description": "Bank Identifier Code / SWIFT code (SEPA markets)", + "pattern": "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$" + }, + "routing_number": { + "type": "string", + "description": "Bank routing number for non-SEPA markets (e.g., US ABA routing number, Canadian transit/institution number)", + "maxLength": 30 + }, + "account_number": { + "type": "string", + "description": "Bank account number for non-SEPA markets", + "maxLength": 30 + } + }, + "required": [ + "account_holder" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "legal_name" + ], + "additionalProperties": false, + "examples": [ + { + "description": "German agency with full B2B details", + "data": { + "legal_name": "Pinnacle Media GmbH", + "vat_id": "DE123456789", + "registration_number": "HRB 12345", + "address": { + "street": "Friedrichstrasse 100", + "city": "Berlin", + "postal_code": "10117", + "country": "DE" + }, + "contacts": [ + { + "role": "billing", + "name": "Sam Adeyemi", + "email": "billing@pinnacle-media.com", + "phone": "+49 30 12345678" + } + ], + "bank": { + "account_holder": "Pinnacle Media GmbH", + "iban": "DE89370400440532013000", + "bic": "COBADEFFXXX" + } + } + }, + { + "description": "US advertiser with EIN and domestic bank details", + "data": { + "legal_name": "Acme Corporation", + "tax_id": "12-3456789", + "address": { + "street": "123 Main St", + "city": "New York", + "postal_code": "10001", + "region": "NY", + "country": "US" + }, + "contacts": [ + { + "role": "billing", + "name": "AP Department", + "email": "ap@acme-corp.com" + } + ], + "bank": { + "account_holder": "Acme Corporation", + "routing_number": "021000021", + "account_number": "123456789" + } + } + } + ] + }, + "rate_card": { + "type": "string", + "description": "Identifier for the rate card applied to this account" + }, + "payment_terms": { + "$ref": "#/$defs/PaymentTerms" + }, + "credit_limit": { + "type": "object", + "description": "Maximum outstanding balance allowed", + "properties": { + "amount": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + } + }, + "required": [ + "amount", + "currency" + ] + }, + "setup": { + "type": "object", + "description": "Present when status is 'pending_approval'. Contains next steps for completing account activation.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL where the human can complete the required action (credit application, legal agreement, add funds)." + }, + "message": { + "type": "string", + "description": "Human-readable description of what's needed." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When this setup link expires." + } + }, + "required": [ + "message" + ], + "additionalProperties": true + }, + "account_scope": { + "$ref": "#/$defs/AccountScope" + }, + "governance_agents": { + "type": "array", + "description": "Governance agent endpoint registered on this account. Exactly one entry per sync_governance's one-agent-per-account invariant. The array shape is preserved for wire compatibility with 3.0; `maxItems: 1` is load-bearing and mirrors the singular `governance_context` on the protocol envelope. Authentication credentials are write-only and not included in responses \u2014 use sync_governance to set or update credentials.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "Governance agent endpoint URL. Must use HTTPS." + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1 + }, + "reporting_bucket": { + "type": "object", + "description": "Cloud storage bucket where the seller delivers offline reporting files for this account. Seller provisions a dedicated bucket or a per-account prefix within a shared bucket, and grants the buyer read access out-of-band. Access MUST be scoped at the IAM layer so each account can only read its own prefix \u2014 bucket-wide grants are non-compliant even with per-account prefixes. Seller MUST revoke access when the account's status transitions to inactive, suspended, or closed. See security considerations for offline delivery in docs/media-buy/media-buys/optimization-reporting. Only present when the seller supports offline delivery (reporting_delivery_methods includes 'offline' in capabilities).", + "properties": { + "protocol": { + "$ref": "#/$defs/CloudStorageProtocol" + }, + "bucket": { + "type": "string", + "description": "Bucket or container name", + "minLength": 3, + "maxLength": 63, + "pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$" + }, + "prefix": { + "type": "string", + "description": "Path prefix within the bucket. Seller appends date-based partitioning beneath this prefix.", + "maxLength": 512, + "pattern": "^[a-zA-Z0-9/_.-]+$", + "examples": [ + "accounts/pinnacle/adcp", + "reporting/2024" + ] + }, + "region": { + "type": "string", + "description": "Cloud region for the bucket", + "maxLength": 64, + "pattern": "^[a-z0-9-]+$", + "examples": [ + "us-east-1", + "europe-west1" + ] + }, + "format": { + "type": "string", + "enum": [ + "jsonl", + "csv", + "parquet", + "avro", + "orc" + ], + "description": "File format for delivered files. Parquet, Avro, and ORC use internal compression (the top-level compression field is ignored for these formats).", + "default": "jsonl" + }, + "compression": { + "type": "string", + "enum": [ + "gzip", + "none" + ], + "description": "Compression applied to delivered files", + "default": "gzip" + }, + "file_retention_days": { + "type": "integer", + "description": "How long reporting files are retained in the bucket before deletion. Buyers must read files within this window. Minimum recommended: 14 days.", + "minimum": 1, + "examples": [ + 14, + 30, + 90 + ] + }, + "setup_instructions": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "URL to documentation for configuring buyer read access to this bucket (IAM role, service account, etc.). Operator-facing documentation \u2014 buyer agents MUST NOT auto-fetch this URL; surface it to a human operator. If an implementation fetches it (for preview), apply webhook URL SSRF validation and do not pass the fetched content into an LLM context without indirect-prompt-injection guarding. See docs/media-buy/media-buys/optimization-reporting#security-considerations-for-offline-delivery." + } + }, + "required": [ + "protocol", + "bucket", + "file_retention_days" + ], + "additionalProperties": false + }, + "sandbox": { + "type": "boolean", + "description": "When true, this is a sandbox account \u2014 no real platform calls, no real spend. For explicit accounts (require_operator_auth: true), sandbox accounts are pre-existing test accounts on the platform discovered via list_accounts. For implicit accounts, sandbox is part of the natural key: the same brand/operator pair can have both a production and sandbox account." + }, + "notification_configs": { + "type": "array", + "description": "Account-level webhook subscriptions for notifications whose lifecycle outlives any single media buy (e.g., `creative.status_changed`, `creative.purged`, wholesale feed change payloads). This is an account-scoped delivery surface, not an account-object lifecycle event stream; account status changes are observed through `list_accounts` polling or the one-shot `sync_accounts.push_notification_config` async result channel. Distinct from `push_notification_config` on individual operations, which anchors at a per-resource scope. Buyers register and update entries via `sync_accounts`; sellers echo the applied state here on `list_accounts` reads so buyers can verify what's active. The set is keyed by account-scoped `subscriber_id`; re-registering the same `subscriber_id` replaces that subscriber's config. `authentication.credentials` is write-only \u2014 sellers MUST NOT echo legacy auth credentials in this response. When two or more entries register the same `event_types`, each receives an independent fire \u2014 see #3009 multi-subscriber composition.", + "items": { + "title": "Notification Config", + "description": "Account-level webhook subscription for notifications whose lifecycle outlives any single media buy \u2014 creative state changes, library purges, wholesale feed change webhooks, and future account-anchored resource events after those event types are added to notification-type.json. This surface does not currently carry lifecycle events for the account object itself (for example, there is no `account.status_changed` event type); account status changes are observed through `list_accounts` polling or the one-shot `sync_accounts.push_notification_config` async result channel. Distinct from `push-notification-config.json`, which anchors at a per-resource operation (a single task or media buy). An account MAY register multiple notification configs to fan a single seller's events out to multiple buyer-side endpoints; each entry filters by `event_types`. As with push-notification-config, the default signing scheme is the AdCP RFC 9421 webhook profile against the seller's brand.json `agents[]` JWKS; the optional `authentication` block opts into the deprecated Bearer / HMAC-SHA256 fallback for compatibility. Credentials and shared secrets in `authentication.credentials` are write-only \u2014 sellers MUST NOT echo them back in `list_accounts` responses. Sellers MUST verify endpoint control before activating a new or changed active account-level notification config; delivery-time SSRF validation still applies to every fire.", + "type": "object", + "properties": { + "subscriber_id": { + "type": "string", + "description": "Buyer-supplied identifier for this subscription endpoint. This is the stable logical key within one account's notification_configs[] set: re-sending the same subscriber_id for the same account replaces that subscriber's URL, event_types, authentication selector, and active flag rather than creating a duplicate. Echoed on every webhook payload and on every `webhook_activity[]` record fired against this config so the buyer can attribute fires across multiple endpoints. MUST be unique within the account's `notification_configs[]`. Sending two entries with the same `subscriber_id` in a single `sync_accounts` request array is rejected as a per-account validation failure with `INVALID_REQUEST` or `VALIDATION_ERROR`, and `error.field` MUST point at the duplicate entry. `subscriber_id` is the stable match key for the per-account declarative-replace diff. Always required (even with a single subscriber) so the SDK contract is uniform \u2014 no conditional required-when-multiple rules to trip up implementations. Format is opaque \u2014 recommended values are short kebab-case slugs (`buyer-primary`, `audit-bus`, `dx-team`).", + "minLength": 1, + "maxLength": 64, + "pattern": "^[A-Za-z0-9_.:-]{1,64}$" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL. Same wire contract as `push-notification-config.url` \u2014 `format: \"uri\"`, no destination-port allowlist enforced by the protocol, SSRF protection via the IP-range check defined in docs/building/by-layer/L1/security.mdx#webhook-url-validation-ssrf. Sellers MUST validate URL syntax, HTTPS usage, hostname normalization, and reserved-range rejection when writing any config, including `active: false` configs. Sellers MUST complete an activation challenge or equivalent proof-of-control before treating a new or changed active subscriber as active." + }, + "event_types": { + "type": "array", + "description": "Notification types this subscriber wishes to receive on the registered `url`. The seller MUST NOT fire other types against this endpoint, and MUST NOT silently widen the filter when new types are added to `notification-type.json`. When omitted, the seller MUST default to a no-fire policy and surface an `errors[]` entry on `sync_accounts` so the buyer notices the missing filter. Values are drawn from `notification-type.json`, but only types whose contract anchors at the account scope are valid here \u2014 creative lifecycle events and wholesale feed change payloads are valid; media-buy-anchored types (`scheduled`, `final`, `delayed`, `adjusted`, `impairment`) and account-lifecycle names not present in the enum (for example, `account.status_changed`) are invalid on this surface; sellers MUST reject those entries as per-account validation failures with `INVALID_REQUEST` or `VALIDATION_ERROR` and `error.field` pointing at the invalid `event_types` entry rather than silently dropping them.", + "items": { + "$ref": "#/$defs/NotificationType" + }, + "minItems": 1, + "uniqueItems": true + }, + "authentication": { + "type": "object", + "description": "Legacy authentication selector. Same precedence and semantics as `push-notification-config.authentication` \u2014 presence opts the seller into Bearer or HMAC-SHA256 signing; absence selects the default RFC 9421 webhook profile keyed off the seller's brand.json `agents[]` JWKS. The same signed-registration downgrade-resistance rules apply to accounts[].notification_configs[].authentication. Deprecated; removed in AdCP 4.0. Credentials are write-only and MUST NOT be echoed on `list_accounts` reads.", + "properties": { + "schemes": { + "type": "array", + "items": { + "$ref": "#/$defs/AuthenticationScheme" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. Bearer: token. HMAC-SHA256: shared secret. Minimum 32 characters. Exchanged out-of-band during onboarding. Write-only.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + }, + "active": { + "type": "boolean", + "default": true, + "description": "When false, the seller persists the configuration but suppresses fires. Use to pause a noisy subscriber without losing the registration. Sellers MUST NOT skip persisting the entry when `active: false` \u2014 the buyer's next `sync_accounts` MUST observe the same array, otherwise the buyer cannot distinguish pause from drop. Paused configs may skip only the outbound proof challenge while inactive; sellers MUST still enforce URL parsing, HTTPS, hostname normalization, and reserved-range rejection at write time. Reactivation requires full SSRF validation with connect pinning plus proof-of-control for any tuple without current valid proof." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "subscriber_id", + "url", + "event_types" + ], + "additionalProperties": false, + "examples": [ + { + "description": "Single subscriber receiving creative lifecycle events", + "data": { + "subscriber_id": "buyer-primary", + "url": "https://buyer.example/webhooks/adcp/creative", + "event_types": [ + "creative.status_changed", + "creative.purged" + ], + "active": true + } + }, + { + "description": "Audit bus subscriber (one of multiple entries on the same account; the array lives on the account, this is a single item)", + "data": { + "subscriber_id": "audit-bus", + "url": "https://audit.buyer.example/adcp/ingest", + "event_types": [ + "creative.status_changed", + "creative.purged" + ], + "active": true + } + }, + { + "description": "Wholesale feed mirror subscriber receiving product and signal change payloads", + "data": { + "subscriber_id": "wholesale-feed-sync", + "url": "https://buyer.example/webhooks/adcp/wholesale-feed", + "event_types": [ + "product.created", + "product.updated", + "product.priced", + "product.removed", + "signal.created", + "signal.updated", + "signal.priced", + "signal.removed", + "wholesale_feed.bulk_change" + ], + "active": true + } + }, + { + "description": "Legacy Bearer authentication (deprecated, 4.0 removal)", + "data": { + "subscriber_id": "buyer-primary", + "url": "https://buyer.example/webhooks/adcp", + "event_types": [ + "creative.status_changed" + ], + "authentication": { + "schemes": [ + "Bearer" + ], + "credentials": "AbCdEf0123456789AbCdEf0123456789" + }, + "active": true + } + } + ] + }, + "maxItems": 16 + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "account_id", + "name", + "status" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Direct advertiser account", + "data": { + "account_id": "acc_acme_direct", + "name": "Acme", + "advertiser": "Acme Corp", + "brand": { + "domain": "acme-corp.com" + }, + "operator": "acme-corp.com", + "status": "active", + "billing": "operator", + "account_scope": "brand", + "rate_card": "acme_vip_2024", + "payment_terms": "net_30" + } + }, + { + "description": "Advertiser account with agency billing proxy", + "data": { + "account_id": "acc_acme_pinnacle", + "name": "Acme c/o Pinnacle", + "advertiser": "Acme Corp", + "billing_proxy": "Pinnacle Media", + "brand": { + "domain": "acme-corp.com" + }, + "operator": "pinnacle-media.com", + "status": "active", + "billing": "operator", + "account_scope": "operator_brand", + "rate_card": "acme_vip_2024", + "payment_terms": "net_60" + } + }, + { + "description": "Agency as direct buyer", + "data": { + "account_id": "acc_pinnacle", + "name": "Pinnacle", + "advertiser": "Pinnacle Media", + "brand": { + "domain": "pinnacle-media.com" + }, + "operator": "pinnacle-media.com", + "status": "active", + "billing": "operator", + "account_scope": "operator", + "rate_card": "agency_standard", + "payment_terms": "net_45" + } + }, + { + "description": "Account with brand identity and operator (via sync_accounts)", + "data": { + "account_id": "acc_spark_001", + "name": "Spark (via Pinnacle)", + "advertiser": "Nova Brands", + "status": "active", + "brand": { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + "operator": "pinnacle-media.com", + "billing": "agent", + "account_scope": "operator_brand", + "payment_terms": "net_30" + } + }, + { + "description": "Pending account awaiting seller approval", + "data": { + "account_id": "acc_glow_pending", + "name": "Glow", + "advertiser": "Nova Brands", + "status": "pending_approval", + "brand": { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + "operator": "pinnacle-media.com", + "billing": "operator", + "account_scope": "brand" + } + }, + { + "description": "Agency operates but advertiser is billed directly with structured billing entity", + "data": { + "account_id": "acc_acme_direct_bill", + "name": "Acme (billed direct)", + "advertiser": "Acme Corp", + "brand": { + "domain": "acme-corp.com" + }, + "operator": "pinnacle-media.com", + "status": "active", + "billing": "advertiser", + "billing_entity": { + "legal_name": "Acme Corporation GmbH", + "vat_id": "DE987654321", + "address": { + "street": "Hauptstrasse 42", + "city": "Munich", + "postal_code": "80331", + "country": "DE" + }, + "contacts": [ + { + "role": "billing", + "name": "AP Department", + "email": "billing@acme-corp.com" + } + ] + }, + "account_scope": "operator_brand", + "payment_terms": "net_30" + } + } + ] + }, + "invoice_recipient": { + "title": "Business Entity", + "description": "Per-buy invoice recipient, echoed from the request when provided. Confirms the seller accepted the billing override. Bank details are omitted (write-only).", + "type": "object", + "properties": { + "legal_name": { + "type": "string", + "description": "Registered legal name of the business entity", + "maxLength": 200 + }, + "vat_id": { + "type": "string", + "description": "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.", + "pattern": "^[A-Z]{2}[A-Z0-9]{2,13}$" + }, + "tax_id": { + "type": "string", + "description": "Tax identification number for jurisdictions that do not use VAT (e.g., US EIN)", + "maxLength": 30 + }, + "registration_number": { + "type": "string", + "description": "Company registration number (e.g., HRB 12345 for German Handelsregister)", + "maxLength": 50 + }, + "address": { + "type": "object", + "description": "Postal address for invoicing and legal correspondence", + "properties": { + "street": { + "type": "string", + "description": "Street address including building number", + "maxLength": 200 + }, + "city": { + "type": "string", + "maxLength": 100 + }, + "postal_code": { + "type": "string", + "maxLength": 20 + }, + "region": { + "type": "string", + "description": "State, province, or region", + "maxLength": 100 + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code", + "pattern": "^[A-Z]{2}$" + } + }, + "required": [ + "street", + "city", + "postal_code", + "country" + ], + "additionalProperties": false + }, + "contacts": { + "type": "array", + "description": "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.", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "billing", + "legal", + "creative", + "general" + ], + "enumDescriptions": { + "billing": "Accounts payable and invoice queries", + "legal": "Contract and compliance matters", + "creative": "Material submission and creative approval", + "general": "Default contact when no specific role applies" + }, + "description": "Contact's functional role in the business relationship" + }, + "name": { + "type": "string", + "description": "Full name of the contact", + "maxLength": 200 + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 254 + }, + "phone": { + "type": "string", + "maxLength": 30 + } + }, + "required": [ + "role" + ], + "additionalProperties": false + }, + "maxItems": 10 + }, + "bank": { + "type": "object", + "writeOnly": true, + "description": "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.", + "properties": { + "account_holder": { + "type": "string", + "description": "Name on the bank account", + "maxLength": 200 + }, + "iban": { + "type": "string", + "description": "International Bank Account Number (SEPA markets)", + "pattern": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$" + }, + "bic": { + "type": "string", + "description": "Bank Identifier Code / SWIFT code (SEPA markets)", + "pattern": "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$" + }, + "routing_number": { + "type": "string", + "description": "Bank routing number for non-SEPA markets (e.g., US ABA routing number, Canadian transit/institution number)", + "maxLength": 30 + }, + "account_number": { + "type": "string", + "description": "Bank account number for non-SEPA markets", + "maxLength": 30 + } + }, + "required": [ + "account_holder" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "legal_name" + ], + "additionalProperties": false, + "examples": [ + { + "description": "German agency with full B2B details", + "data": { + "legal_name": "Pinnacle Media GmbH", + "vat_id": "DE123456789", + "registration_number": "HRB 12345", + "address": { + "street": "Friedrichstrasse 100", + "city": "Berlin", + "postal_code": "10117", + "country": "DE" + }, + "contacts": [ + { + "role": "billing", + "name": "Sam Adeyemi", + "email": "billing@pinnacle-media.com", + "phone": "+49 30 12345678" + } + ], + "bank": { + "account_holder": "Pinnacle Media GmbH", + "iban": "DE89370400440532013000", + "bic": "COBADEFFXXX" + } + } + }, + { + "description": "US advertiser with EIN and domestic bank details", + "data": { + "legal_name": "Acme Corporation", + "tax_id": "12-3456789", + "address": { + "street": "123 Main St", + "city": "New York", + "postal_code": "10001", + "region": "NY", + "country": "US" + }, + "contacts": [ + { + "role": "billing", + "name": "AP Department", + "email": "ap@acme-corp.com" + } + ], + "bank": { + "account_holder": "Acme Corporation", + "routing_number": "021000021", + "account_number": "123456789" + } + } + } + ] + }, + "media_buy_status": { + "$ref": "#/$defs/MediaBuyStatus" + }, + "status": { + "$ref": "#/$defs/MediaBuyStatus" + }, + "confirmed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this media buy was confirmed by the seller. A successful create_media_buy response constitutes order confirmation." + }, + "creative_deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp for creative upload deadline" + }, + "revision": { + "type": "integer", + "description": "Initial revision number for this media buy. Use in subsequent update_media_buy requests for optimistic concurrency.", + "minimum": 1 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code (e.g., USD, EUR, GBP) for monetary values at this media buy level. total_budget is denominated in this currency. Package-level fields may override with package.currency. In proposal mode the seller derives this from the request's total_budget object (total_budget.currency); in manual mode it is present when all packages share a currency. Matches the currency field in subsequent get_media_buys responses.", + "pattern": "^[A-Z]{3}$" + }, + "total_budget": { + "type": "number", + "description": "Total budget amount across all packages, denominated in currency. Note: the create_media_buy request encodes total_budget as an object {amount, currency} (proposal mode only); this response field is the flattened scalar amount, with currency promoted to the sibling currency field. Present when the seller can compute a deterministic aggregate \u2014 always in proposal mode, conditionally in manual mode when all packages share a currency. Matches the total_budget field in subsequent get_media_buys responses.", + "minimum": 0 + }, + "valid_actions": { + "type": "array", + "description": "Flat-vocabulary actions the buyer can perform on this media buy after creation. Saves a round-trip to get_media_buys. Deprecated in favor of `available_actions[]`, which carries `mode`, optional SLA, and optional `terms_ref`. Sellers SHOULD populate both during the 3.x deprecation window; consumers MUST prefer `available_actions[]` when both are present. Removed in 4.0.", + "items": { + "$ref": "#/$defs/MediaBuyValidAction" + } + }, + "available_actions": { + "type": "array", + "description": "Structured per-buy resolution of actions available immediately after creation. Authoritative \u2014 see `get-media-buys-response.json` for full semantics.", + "items": { + "title": "Media Buy Available Action", + "description": "An action currently available on a media buy, resolved against the buy's current status, negotiated terms, account tier, and any buy-level overrides. Authoritative per-buy capability \u2014 buyer SDKs MUST read this rather than re-deriving from the product's `allowed_actions[]`, because divergence from the product template is expected (negotiated terms and IO addenda live on the deal, not the product SKU). The containing `available_actions[]` array is uniquely keyed by `action`; sellers MUST NOT emit two entries with the same `action` value (this is a contract-level invariant \u2014 JSON Schema `uniqueItems` only catches structurally identical objects, so validators MUST enforce action-uniqueness separately). Predicate evaluators consuming dotted paths like `available_actions.extend_flight.sla.response_max` MUST index by `action` rather than by array position. The `mode` and `sla` values are advisory at the moment of emission; sellers MAY resolve to a different mode by the time the mutation arrives (state can change), in which case the request is rejected with `ACTION_NOT_ALLOWED` (`reason: mode_mismatch`).", + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/MediaBuyValidAction" + }, + "mode": { + "$ref": "#/$defs/MediaBuyActionMode" + }, + "sla": { + "title": "SLA Window", + "description": "Optional SLA commitment for this action on this buy. Absence means no commitment, not zero commitment.", + "type": "object", + "properties": { + "response_max": { + "type": "string", + "description": "Maximum time from when the buyer issues the action to when the seller acknowledges receipt (mode-appropriate: synchronous response for self_serve, queue ack for requires_approval, proposal task creation for requires_proposal). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT5M", + "PT4H", + "P1D" + ] + }, + "completion_max": { + "type": "string", + "description": "Maximum time from buyer issuing the action to the seller completing it (mutation applied, proposal finalized, approval resolved). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT1H", + "PT24H", + "P2D" + ] + } + }, + "additionalProperties": false + }, + "terms_ref": { + "type": "string", + "description": "Optional pointer into buy-terms negotiation (forward-references the buy-terms namespace landing via separate RFC). Schema accepts any string for now and will tighten to a structured reference when the buy-terms RFC ships." + } + }, + "required": [ + "action", + "mode" + ], + "additionalProperties": false + }, + "uniqueItems": true + }, + "packages": { + "type": "array", + "description": "Array of created packages with complete state information", + "items": { + "title": "Package", + "description": "A specific product within a media buy (line item)", + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Seller's unique identifier for the package", + "x-entity": "package" + }, + "product_id": { + "type": "string", + "description": "ID of the product this package is based on", + "x-entity": "product" + }, + "budget": { + "type": "number", + "description": "Budget allocation for this package in the currency specified by the pricing option", + "minimum": 0 + }, + "pacing": { + "$ref": "#/$defs/Pacing" + }, + "pricing_option_id": { + "type": "string", + "description": "ID of the selected pricing option from the product's pricing_options array", + "x-entity": "product_pricing_option" + }, + "bid_price": { + "type": "number", + "description": "Bid price for auction-based pricing. This is the exact bid/price to honor unless the selected pricing option has max_bid=true, in which case bid_price is the buyer's maximum willingness to pay (ceiling).", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "impressions": { + "type": "number", + "description": "Impression goal for this package", + "minimum": 0 + }, + "catalogs": { + "type": "array", + "description": "Catalogs this package promotes. Each catalog MUST have a distinct type (e.g., one product catalog, one store catalog). This constraint is enforced at the application level \u2014 sellers MUST reject requests containing multiple catalogs of the same type with a validation_error. Echoed from the create_media_buy request.", + "items": { + "title": "Catalog", + "description": "A typed data feed. Catalogs carry the items, locations, stock levels, or pricing that publishers use to render ads. They can be synced to a platform via sync_catalogs (managed lifecycle with approval), provided inline, or fetched from an external URL. The catalog type determines the item schema and can be structural (offering, product, inventory, store, promotion) or vertical-specific (hotel, flight, job, vehicle, real_estate, education, destination, app). Selectors (ids, tags, category, query) filter items regardless of sourcing method.", + "type": "object", + "properties": { + "catalog_id": { + "type": "string", + "description": "Buyer's identifier for this catalog. Required when syncing via sync_catalogs. When used in creatives, references a previously synced catalog on the account.", + "x-entity": "catalog" + }, + "name": { + "type": "string", + "description": "Human-readable name for this catalog (e.g., 'Summer Products 2025', 'Amsterdam Store Locations')." + }, + "type": { + "$ref": "#/$defs/CatalogType" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to an external catalog feed. The platform fetches and resolves items from this URL. For offering-type catalogs, the feed contains an array of Offering objects. For other types, the feed format is determined by feed_format. When omitted with type 'product', the platform uses its synced copy of the brand's product catalog." + }, + "feed_format": { + "$ref": "#/$defs/FeedFormat" + }, + "update_frequency": { + "$ref": "#/$defs/UpdateFrequency" + }, + "items": { + "type": "array", + "description": "Inline catalog data. The item schema depends on the catalog type: Offering objects for 'offering', StoreItem for 'store', HotelItem for 'hotel', FlightItem for 'flight', JobItem for 'job', VehicleItem for 'vehicle', RealEstateItem for 'real_estate', EducationItem for 'education', DestinationItem for 'destination', AppItem for 'app', or freeform objects for 'product', 'inventory', and 'promotion'. Mutually exclusive with url \u2014 provide one or the other, not both. Implementations should validate items against the type-specific schema.", + "items": { + "type": "object" + }, + "minItems": 1 + }, + "ids": { + "type": "array", + "description": "Filter catalog to specific item IDs. For offering-type catalogs, these are offering_id values. For product-type catalogs, these are SKU identifiers.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "gtins": { + "type": "array", + "description": "Filter product-type catalogs by GTIN identifiers for cross-retailer catalog matching. Accepts standard GTIN formats (GTIN-8, UPC-A/GTIN-12, EAN-13/GTIN-13, GTIN-14). Only applicable when type is 'product'.", + "items": { + "type": "string", + "pattern": "^[0-9]{8,14}$" + }, + "minItems": 1 + }, + "tags": { + "type": "array", + "description": "Filter catalog to items with these tags. Tags are matched using OR logic \u2014 items matching any tag are included.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "category": { + "type": "string", + "description": "Filter catalog to items in this category (e.g., 'beverages/soft-drinks', 'chef-positions')." + }, + "query": { + "type": "string", + "description": "Natural language filter for catalog items (e.g., 'all pasta sauces under $5', 'amsterdam vacancies')." + }, + "conversion_events": { + "type": "array", + "description": "Event types that represent conversions for items in this catalog. Declares what events the platform should attribute to catalog items \u2014 e.g., a job catalog converts via submit_application, a product catalog via purchase. The event's content_ids field carries the item IDs that connect back to catalog items. Use content_id_type to declare what identifier type content_ids values represent.", + "items": { + "$ref": "#/$defs/EventType" + }, + "minItems": 1, + "uniqueItems": true + }, + "content_id_type": { + "$ref": "#/$defs/ContentIDType" + }, + "feed_field_mappings": { + "type": "array", + "description": "Declarative normalization rules for external feeds. Maps non-standard feed field names, date formats, price encodings, and image URLs to the AdCP catalog item schema. Applied during sync_catalogs ingestion. Supports field renames, named transforms (date, divide, boolean, split), static literal injection, and assignment of image URLs to typed asset pools.", + "items": { + "title": "Catalog Field Mapping", + "description": "Declares how a field in an external feed maps to the AdCP catalog item schema. Used in sync_catalogs feed_field_mappings to normalize non-AdCP feeds (Google Merchant Center, LinkedIn Jobs XML, hotel XML, etc.) to the standard catalog item schema without requiring the buyer to preprocess every feed. Multiple mappings can assemble a nested object via dot notation (e.g., separate mappings for price.amount and price.currency).", + "type": "object", + "properties": { + "feed_field": { + "type": "string", + "description": "Field name in the external feed record. Omit when injecting a static literal value (use the value property instead)." + }, + "catalog_field": { + "type": "string", + "description": "Target field on the catalog item schema, using dot notation for nested fields (e.g., 'name', 'price.amount', 'location.city'). Mutually exclusive with asset_group_id." + }, + "asset_group_id": { + "type": "string", + "description": "Places the feed field value (a URL) into a typed asset pool on the catalog item's assets array. The value is wrapped as an image or video asset in a group with this ID. Use standard group IDs: 'images_landscape', 'images_vertical', 'images_square', 'logo', 'video'. Mutually exclusive with catalog_field." + }, + "value": { + "description": "Static literal value to inject into catalog_field for every item, regardless of what the feed contains. Mutually exclusive with feed_field. Useful for fields the feed omits (e.g., currency when price is always USD, or a constant category value)." + }, + "transform": { + "type": "string", + "description": "Named transform to apply to the feed field value before writing to the catalog schema. See transform-specific parameters (format, timezone, by, separator).", + "enum": [ + "date", + "divide", + "boolean", + "split" + ] + }, + "format": { + "type": "string", + "description": "For transform 'date': the input date format string (e.g., 'YYYYMMDD', 'MM/DD/YYYY', 'DD-MM-YYYY'). Output is always ISO 8601 (e.g., '2025-03-01'). Uses Unicode date pattern tokens." + }, + "timezone": { + "type": "string", + "description": "For transform 'date': the timezone of the input value. IANA timezone identifier (e.g., 'UTC', 'America/New_York', 'Europe/Amsterdam'). Defaults to UTC when omitted." + }, + "by": { + "type": "number", + "description": "For transform 'divide': the divisor to apply (e.g., 100 to convert integer cents to decimal dollars).", + "exclusiveMinimum": 0 + }, + "separator": { + "type": "string", + "description": "For transform 'split': the separator character or string to split on. Defaults to ','.", + "default": "," + }, + "default": { + "description": "Fallback value to use when feed_field is absent, null, or empty. Applied after any transform would have been applied. Allows optional feed fields to have a guaranteed baseline value." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "allOf": [ + { + "not": { + "required": [ + "feed_field", + "value" + ] + } + }, + { + "not": { + "required": [ + "catalog_field", + "asset_group_id" + ] + } + } + ], + "additionalProperties": true, + "examples": [ + { + "description": "Simple field rename", + "data": { + "feed_field": "hotel_name", + "catalog_field": "name" + } + }, + { + "description": "Date format transform \u2014 YYYYMMDD to ISO 8601", + "data": { + "feed_field": "available_from", + "catalog_field": "valid_from", + "transform": "date", + "format": "YYYYMMDD", + "timezone": "UTC" + } + }, + { + "description": "Divide cents to dollars", + "data": { + "feed_field": "price_cents", + "catalog_field": "price.amount", + "transform": "divide", + "by": 100 + } + }, + { + "description": "Static literal injection \u2014 currency not in feed", + "data": { + "catalog_field": "price.currency", + "value": "USD" + } + }, + { + "description": "Image URL assigned to typed asset pool", + "data": { + "feed_field": "primary_photo_url", + "asset_group_id": "images_landscape" + } + }, + { + "description": "Vertical photo URL assigned to vertical pool", + "data": { + "feed_field": "portrait_photo_url", + "asset_group_id": "images_vertical" + } + }, + { + "description": "Split comma-separated tags to array", + "data": { + "feed_field": "amenity_list", + "catalog_field": "amenities", + "transform": "split", + "separator": "," + } + }, + { + "description": "Boolean coercion with default", + "data": { + "feed_field": "is_available", + "catalog_field": "available", + "transform": "boolean", + "default": false + } + } + ] + }, + "minItems": 1 + } + }, + "required": [ + "type" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Synced product catalog from Google Merchant Center", + "data": { + "catalog_id": "gmc-primary", + "name": "Primary Product Feed", + "type": "product", + "url": "https://feeds.acmecorp.com/products.xml", + "feed_format": "google_merchant_center", + "update_frequency": "daily" + } + }, + { + "description": "Inventory feed for store-level stock data", + "data": { + "catalog_id": "store-inventory", + "name": "Store Inventory", + "type": "inventory", + "url": "https://feeds.acmecorp.com/inventory.json", + "feed_format": "custom", + "update_frequency": "hourly" + } + }, + { + "description": "Store locator feed", + "data": { + "catalog_id": "retail-locations", + "name": "Retail Locations", + "type": "store", + "url": "https://feeds.acmecorp.com/stores.json", + "feed_format": "custom", + "update_frequency": "weekly" + } + }, + { + "description": "Promotional pricing feed", + "data": { + "catalog_id": "summer-sale", + "name": "Summer Sale Promotions", + "type": "promotion", + "url": "https://feeds.acmecorp.com/promotions.json", + "feed_format": "google_merchant_center", + "update_frequency": "daily" + } + }, + { + "description": "Inline offering catalog (no sync needed)", + "data": { + "type": "offering", + "items": [ + { + "offering_id": "summer-sale", + "name": "Summer Sale", + "landing_url": "https://acme.com/summer" + } + ] + } + }, + { + "description": "Reference to a previously synced catalog by ID", + "data": { + "catalog_id": "gmc-primary", + "type": "product", + "ids": [ + "SKU-12345", + "SKU-67890" + ] + } + }, + { + "description": "Product catalog with GTIN cross-retailer matching and attribution", + "data": { + "type": "product", + "gtins": [ + "00013000006040", + "00013000006057" + ], + "content_id_type": "gtin", + "conversion_events": [ + "purchase", + "add_to_cart" + ] + } + }, + { + "description": "Inline store catalog with catchment areas", + "data": { + "catalog_id": "retail-locations", + "name": "Retail Locations", + "type": "store", + "items": [ + { + "store_id": "amsterdam-flagship", + "name": "Amsterdam Flagship", + "location": { + "lat": 52.3676, + "lng": 4.9041 + }, + "catchments": [ + { + "catchment_id": "walk", + "travel_time": { + "value": 10, + "unit": "min" + }, + "transport_mode": "walking" + }, + { + "catchment_id": "drive", + "travel_time": { + "value": 15, + "unit": "min" + }, + "transport_mode": "driving" + } + ] + } + ] + } + } + ] + } + }, + "format_ids": { + "type": "array", + "description": "Legacy named-format IDs active for this package. Echoed from the create_media_buy request; omitted means all formats for the product are active unless `format_option_refs` narrows the 3.1+ format-option set.", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + } + }, + "format_option_refs": { + "type": "array", + "description": "Structured 3.1+ format option references active for this package, echoed from the create_media_buy request. Publisher-catalog-backed options are identified by `{ scope: \"publisher\", publisher_domain, format_option_id }`; product-local options are identified by `{ scope: \"product\", format_option_id }` and resolve only against this package's target product. Omitted means all 3.1+ format options for the product are active unless `format_ids` narrows the set.", + "items": { + "title": "Format Option Reference", + "description": "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.", + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "title": "Publisher Catalog Format Option Reference", + "description": "Selects a publisher-catalog-backed product format option by publisher domain and format option ID.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "publisher", + "description": "Reference resolves against the named publisher's adagents.json top-level `formats[]` catalog." + }, + "publisher_domain": { + "type": "string", + "description": "Publisher domain where the adagents.json declaring this format option is hosted.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "format_option_id": { + "type": "string", + "description": "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[]`." + } + }, + "required": [ + "scope", + "publisher_domain", + "format_option_id" + ], + "additionalProperties": true + }, + { + "title": "Product-Local Format Option Reference", + "description": "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.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Reference resolves only against the target product's inline `format_options[]`." + }, + "format_option_id": { + "type": "string", + "description": "Stable format option ID from the target product's inline `format_options[]`." + }, + "publisher_domain": false + }, + "required": [ + "scope", + "format_option_id" + ], + "additionalProperties": true + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "targeting_overlay": { + "title": "Targeting Overlay", + "description": "Optional restriction overlays for media buys. Most targeting should be expressed in the brief and handled by the publisher. These fields are for functional restrictions: geographic (RCT testing, regulatory compliance, proximity targeting), age verification (alcohol, gambling), device platform (app compatibility), language (localization), and keyword targeting (search/retail media).", + "type": "object", + "properties": { + "geo_countries": { + "type": "array", + "description": "Restrict delivery to specific countries. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "geo_countries_exclude": { + "type": "array", + "description": "Exclude specific countries from delivery. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "geo_regions": { + "type": "array", + "description": "Restrict delivery to specific regions/states. ISO 3166-2 subdivision codes (e.g., 'US-CA', 'GB-SCT').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$" + }, + "minItems": 1 + }, + "geo_regions_exclude": { + "type": "array", + "description": "Exclude specific regions/states from delivery. ISO 3166-2 subdivision codes (e.g., 'US-CA', 'GB-SCT').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$" + }, + "minItems": 1 + }, + "geo_metros": { + "type": "array", + "description": "Restrict delivery to specific metro areas. Each entry specifies the classification system and target values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/MetroAreaSystem" + }, + "values": { + "type": "array", + "description": "Metro codes within the system (e.g., ['501', '602'] for Nielsen DMAs)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "geo_metros_exclude": { + "type": "array", + "description": "Exclude specific metro areas from delivery. Each entry specifies the classification system and excluded values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/MetroAreaSystem" + }, + "values": { + "type": "array", + "description": "Metro codes to exclude within the system (e.g., ['501', '602'] for Nielsen DMAs)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "geo_postal_areas": { + "type": "array", + "description": "Restrict delivery to specific postal areas. Each entry specifies the postal system and target values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/PostalCodeSystem" + }, + "values": { + "type": "array", + "description": "Postal codes within the system (e.g., ['10001', '10002'] for us_zip)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "geo_postal_areas_exclude": { + "type": "array", + "description": "Exclude specific postal areas from delivery. Each entry specifies the postal system and excluded values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/PostalCodeSystem" + }, + "values": { + "type": "array", + "description": "Postal codes to exclude within the system (e.g., ['10001', '10002'] for us_zip)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "daypart_targets": { + "type": "array", + "description": "Restrict delivery to specific time windows. Each entry specifies days of week and an hour range.", + "items": { + "title": "Daypart Target", + "description": "A time window for daypart targeting. Specifies days of week and an hour range. start_hour is inclusive, end_hour is exclusive (e.g., 6-10 = 6:00am to 10:00am). Follows the Google Ads AdScheduleInfo / DV360 DayPartTargeting pattern.", + "type": "object", + "properties": { + "days": { + "type": "array", + "description": "Days of week this window applies to. Use multiple days for compact targeting (e.g., monday-friday in one object).", + "items": { + "$ref": "#/$defs/DayOfWeek" + }, + "minItems": 1 + }, + "start_hour": { + "type": "integer", + "description": "Start hour (inclusive), 0-23 in 24-hour format. 0 = midnight, 6 = 6:00am, 18 = 6:00pm.", + "minimum": 0, + "maximum": 23 + }, + "end_hour": { + "type": "integer", + "description": "End hour (exclusive), 1-24 in 24-hour format. 10 = 10:00am, 24 = midnight. Must be greater than start_hour.", + "minimum": 1, + "maximum": 24 + }, + "label": { + "type": "string", + "description": "Optional human-readable name for this time window (e.g., 'Morning Drive', 'Prime Time')" + } + }, + "required": [ + "days", + "start_hour", + "end_hour" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "axe_include_segment": { + "type": "string", + "description": "Deprecated: Use TMP provider fields instead. AXE segment ID to include for targeting.", + "deprecated": true + }, + "axe_exclude_segment": { + "type": "string", + "description": "Deprecated: Use TMP provider fields instead. AXE segment ID to exclude from targeting.", + "deprecated": true + }, + "audience_include": { + "type": "array", + "description": "Restrict delivery to members of these first-party CRM audiences. Only users present in the uploaded lists are eligible. References audience_id values from sync_audiences on the same seller account \u2014 audience IDs are not portable across sellers. Not for lookalike expansion \u2014 express that intent in the campaign brief. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "audience_exclude": { + "type": "array", + "description": "Suppress delivery to members of these first-party CRM audiences. Matched users are excluded regardless of other targeting. References audience_id values from sync_audiences on the same seller account \u2014 audience IDs are not portable across sellers. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "signal_targeting_groups": { + "title": "Package Signal Targeting Groups", + "description": "Basic Boolean grouping for seller-offered signals. v1 supports a required top-level operator 'all' and child groups with operator 'any' for include groups or 'none' for exclusion groups. Example semantics: group 1 any(A, B) plus group 2 none(C, D) means (A OR B) AND NOT (C OR D). Signal entries reference named signal definitions with signal_ref scope 'product' for product-local signal options or scope 'data_provider' for external published adagents.json signal catalogs. For simple include-only targeting, send one child group with operator 'any'. Sellers SHOULD reject entries that are not available for the product through inline signal_targeting_options or get_signals, are not active for the account, or exceed the product's signal_targeting_allowed/signal_targeting_rules/product terms. Signal targeting limits are product-scoped, not declared in get_adcp_capabilities, because products may be backed by different ad servers. Sellers MUST echo applied signal_targeting_groups on the resulting package state, including fixed/default selections. On update_media_buy, sellers MAY reject changes that require repricing with REQUOTE_REQUIRED.", + "type": "object", + "properties": { + "operator": { + "type": "string", + "enum": [ + "all" + ], + "description": "Groups-level operator. Required even though v1 only supports 'all': every child group must be satisfied." + }, + "groups": { + "type": "array", + "description": "Signal targeting groups to evaluate. Use operator 'any' for include groups and 'none' for exclusion groups.", + "items": { + "title": "Package Signal Targeting Group", + "description": "A basic Boolean group of package-level signal targeting entries. 'any' means the user must match at least one signal in the group. 'none' means the user must match none of the signals in the group. Use groups for portable include/exclude composition such as (A OR B) AND NOT (C OR D).", + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "How to evaluate the signals in this group. 'any' is an OR include group. 'none' is an exclusion group equivalent to NOT (A OR B OR C).", + "enum": [ + "any", + "none" + ] + }, + "signals": { + "type": "array", + "description": "Signal targeting entries evaluated by this group. Each entry uses the package signal targeting shape, including signal_ref, value expression, and optional pricing, execution-handle, or activation fields.", + "items": { + "title": "Package Signal Targeting", + "description": "Buy-time selection of one seller-offered signal inside a package signal targeting group. The signal_ref uses scope 'product' for a product-local signal option, scope 'data_provider' for a signal defined by a data provider's published adagents.json signal catalog, or scope 'signal_source' for a source-native signal that is not catalog-published. The selected product's inline Product.signal_targeting_options, get_signals feed, and signal_targeting_rules define buy-time eligibility. Inclusion and exclusion are controlled by the parent group operator: use operator 'any' to include users matching the signal expression and operator 'none' to exclude users matching the signal expression. For binary signals, value MUST be true; do not use value=false for exclusion inside signal_targeting_groups. Use audience_include/audience_exclude only for buyer-managed first-party audiences registered through sync_audiences.", + "type": "object", + "allOf": [ + { + "title": "Signal Targeting Expression", + "description": "Predicate over a named signal definition. Signals are typed dimensions, similar to feature values: binary signals match true, categorical signals match one of a set of values, and numeric signals match a range. In package signal targeting groups, include/exclude semantics are controlled by the parent group operator, not by negating the expression.", + "discriminator": { + "propertyName": "value_type" + }, + "oneOf": [ + { + "type": "object", + "description": "Binary signal expression. In grouped package targeting, value is always true; use a parent group with operator 'none' for exclusion.", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Named signal being targeted.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "binary", + "description": "Discriminator for binary signals." + }, + "value": { + "type": "boolean", + "const": true, + "description": "Binary package signal entries match users for whom the signal is true. Use the parent group operator for include/exclude." + } + }, + "required": [ + "signal_ref", + "value_type", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Categorical signal expression - target users with one of the listed values.", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Named signal being targeted.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "categorical", + "description": "Discriminator for categorical signals." + }, + "values": { + "type": "array", + "description": "Values to target. Users with any of these values match the expression.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "signal_ref", + "value_type", + "values" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Numeric signal expression - target users within a value range. At least one of min_value or max_value is required. If both min_value and max_value are provided, min_value MUST be <= max_value.", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Named signal being targeted.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "numeric", + "description": "Discriminator for numeric signals." + }, + "min_value": { + "type": "number", + "description": "Minimum value, inclusive. Omit for no minimum. Should be within the signal definition's range when declared." + }, + "max_value": { + "type": "number", + "description": "Maximum value, inclusive. Omit for no maximum. Should be within the signal definition's range when declared." + } + }, + "required": [ + "signal_ref", + "value_type" + ], + "anyOf": [ + { + "required": [ + "min_value" + ] + }, + { + "required": [ + "max_value" + ] + } + ], + "additionalProperties": true + } + ] + } + ], + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Pricing option selected for this signal. Use the pricing_option_id from the product's signal_targeting_options entry when product-scoped pricing is present; otherwise use the seller get_signals pricing only when the product option does not override it. Required when the selected signal has pricing_options; omit only when the signal is bundled into the product price or has no incremental cost.", + "x-entity": "vendor_pricing_option" + }, + "signal_agent_segment_id": { + "type": "string", + "description": "Optional opaque seller execution handle for this signal. Omit when signal_ref is sufficient for the seller to resolve the signal. Include only when the product option exposes a separate runtime or activation handle that differs from the named signal reference.", + "x-entity": "signal_activation_id" + }, + "activation_key": { + "title": "Activation Key", + "description": "Destination-specific activation key returned by get_signals or activate_signal. Usually omitted for seller-offered signals selected directly through the same seller; include only when the selected signal was separately activated and the seller requires the activation key to correlate the package selection.", + "type": "object", + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "properties": { + "type": { + "type": "string", + "const": "segment_id", + "description": "Segment ID based targeting" + }, + "segment_id": { + "type": "string", + "description": "The platform-specific segment identifier to use in campaign targeting" + } + }, + "required": [ + "type", + "segment_id" + ], + "additionalProperties": true + }, + { + "properties": { + "type": { + "type": "string", + "const": "key_value", + "description": "Key-value pair based targeting" + }, + "key": { + "type": "string", + "description": "The targeting parameter key" + }, + "value": { + "type": "string", + "description": "The targeting parameter value" + } + }, + "required": [ + "type", + "key", + "value" + ], + "additionalProperties": true + } + ] + } + }, + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "operator", + "signals" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "operator", + "groups" + ], + "additionalProperties": true + }, + "frequency_cap": { + "title": "Frequency Cap", + "description": "Frequency capping settings for package-level application. Two types of frequency control can be used independently or together: suppress enforces a cooldown between consecutive exposures; max_impressions + per + window caps total exposures per entity in a time window. When both suppress and max_impressions are set, an impression is delivered only if both constraints permit it (AND semantics). At least one of suppress, suppress_minutes, or max_impressions must be set.", + "type": "object", + "properties": { + "suppress": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Cooldown period between consecutive exposures to the same entity. Prevents back-to-back ad delivery (e.g. {\"interval\": 60, \"unit\": \"minutes\"} for a 1-hour cooldown). Preferred over suppress_minutes." + }, + "suppress_minutes": { + "type": "number", + "description": "Deprecated \u2014 use suppress instead. Cooldown period in minutes between consecutive exposures to the same entity (e.g. 60 for a 1-hour cooldown).", + "minimum": 0 + }, + "max_impressions": { + "type": "integer", + "description": "Maximum number of impressions per entity per window. For duration windows, implementations typically use a rolling window; 'campaign' applies a fixed cap across the full flight.", + "minimum": 1 + }, + "per": { + "allOf": [ + { + "$ref": "#/$defs/ReachUnit" + } + ], + "description": "Entity granularity for impression counting. Required when max_impressions is set." + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Time window for the max_impressions cap (e.g. {\"interval\": 7, \"unit\": \"days\"} or {\"interval\": 1, \"unit\": \"campaign\"} for the full flight). Required when max_impressions is set." + } + }, + "anyOf": [ + { + "required": [ + "suppress" + ] + }, + { + "required": [ + "suppress_minutes" + ] + }, + { + "required": [ + "max_impressions" + ] + } + ], + "dependencies": { + "max_impressions": [ + "per", + "window" + ], + "per": [ + "max_impressions" + ], + "window": [ + "max_impressions" + ] + }, + "additionalProperties": true + }, + "property_list": { + "title": "Property List Reference", + "description": "Reference to a property list for targeting specific properties within this product. The package runs on the intersection of the product's publisher_properties and this list. Sellers SHOULD return a validation error if the product has property_targeting_allowed: false.", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent managing the property list" + }, + "list_id": { + "type": "string", + "description": "Identifier for the property list within the agent", + "minLength": 1, + "x-entity": "property_list" + }, + "auth_token": { + "type": "string", + "description": "JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access." + } + }, + "required": [ + "agent_url", + "list_id" + ], + "additionalProperties": false + }, + "collection_list": { + "title": "Collection List Reference", + "description": "Reference to a collection list for including specific collections (programs, shows) within this product. The package runs on the intersection of matched collections and this list. Use for inclusion-based collection targeting. Seller must declare support in get_adcp_capabilities.", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent managing the collection list" + }, + "list_id": { + "type": "string", + "description": "Identifier for the collection list within the agent", + "minLength": 1, + "x-entity": "collection_list" + }, + "auth_token": { + "type": "string", + "description": "JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access." + } + }, + "required": [ + "agent_url", + "list_id" + ], + "additionalProperties": false + }, + "collection_list_exclude": { + "title": "Collection List Reference", + "description": "Reference to a collection list for excluding specific collections (programs, shows) from this product. Matched collections must not carry the buyer's ads. Use for brand safety do-not-air lists. Seller must declare support in get_adcp_capabilities.", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent managing the collection list" + }, + "list_id": { + "type": "string", + "description": "Identifier for the collection list within the agent", + "minLength": 1, + "x-entity": "collection_list" + }, + "auth_token": { + "type": "string", + "description": "JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access." + } + }, + "required": [ + "agent_url", + "list_id" + ], + "additionalProperties": false + }, + "age_restriction": { + "type": "object", + "description": "Age restriction for compliance. Use for legal requirements (alcohol, gambling), not audience targeting.", + "properties": { + "min": { + "type": "integer", + "minimum": 13, + "maximum": 99, + "description": "Minimum age required" + }, + "verification_required": { + "type": "boolean", + "default": false, + "description": "Whether verified age (not inferred) is required for compliance" + }, + "accepted_methods": { + "type": "array", + "description": "Accepted verification methods. If omitted, any method the platform supports is acceptable.", + "items": { + "$ref": "#/$defs/AgeVerificationMethod" + }, + "minItems": 1 + } + }, + "required": [ + "min" + ], + "additionalProperties": false + }, + "device_platform": { + "type": "array", + "description": "Restrict to specific platforms. Use for technical compatibility (app only works on iOS). Values from Sec-CH-UA-Platform standard, extended for CTV.", + "items": { + "$ref": "#/$defs/DevicePlatform" + }, + "minItems": 1 + }, + "device_type": { + "type": "array", + "description": "Restrict to specific device form factors. Use for campaigns targeting hardware categories rather than operating systems (e.g., mobile-only promotions, CTV campaigns).", + "items": { + "$ref": "#/$defs/DeviceType" + }, + "minItems": 1 + }, + "device_type_exclude": { + "type": "array", + "description": "Exclude specific device form factors from delivery (e.g., exclude CTV for app-install campaigns).", + "items": { + "$ref": "#/$defs/DeviceType" + }, + "minItems": 1 + }, + "store_catchments": { + "type": "array", + "description": "Target users within store catchment areas from a synced store catalog. Each entry references a store-type catalog and optionally narrows to specific stores or catchment zones.", + "items": { + "type": "object", + "properties": { + "catalog_id": { + "type": "string", + "description": "Synced store-type catalog ID from sync_catalogs." + }, + "store_ids": { + "type": "array", + "description": "Filter to specific stores within the catalog. Omit to target all stores.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "catchment_ids": { + "type": "array", + "description": "Catchment zone IDs to target (e.g., 'walk', 'drive'). Omit to target all catchment zones.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "catalog_id" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "geo_proximity": { + "type": "array", + "description": "Target users within travel time, distance, or a custom boundary around arbitrary geographic points. Multiple entries use OR semantics \u2014 a user within range of any listed point is eligible. For campaigns targeting 10+ locations, consider using store_catchments with a location catalog instead. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "lat": { + "type": "number", + "minimum": -90, + "maximum": 90, + "description": "Latitude in decimal degrees (WGS 84). Required for travel_time and radius methods." + }, + "lng": { + "type": "number", + "minimum": -180, + "maximum": 180, + "description": "Longitude in decimal degrees (WGS 84). Required for travel_time and radius methods." + }, + "label": { + "type": "string", + "description": "Human-readable label for this entry (e.g., 'D\u00fcsseldorf', 'Heathrow Airport', 'Primary trade area')." + }, + "travel_time": { + "type": "object", + "description": "Travel time limit for isochrone calculation. The platform resolves this to a geographic boundary based on actual transportation networks.", + "properties": { + "value": { + "type": "number", + "minimum": 1, + "description": "Travel time limit." + }, + "unit": { + "$ref": "#/$defs/TravelTimeUnit" + } + }, + "required": [ + "value", + "unit" + ], + "additionalProperties": false + }, + "transport_mode": { + "$ref": "#/$defs/TransportMode" + }, + "radius": { + "type": "object", + "description": "Simple radius from the point. The platform draws a circle of this distance around the coordinates.", + "properties": { + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Radius distance." + }, + "unit": { + "$ref": "#/$defs/DistanceUnit" + } + }, + "required": [ + "value", + "unit" + ], + "additionalProperties": false + }, + "geometry": { + "type": "object", + "description": "Pre-computed GeoJSON geometry defining the proximity boundary. Use when the buyer has already calculated isochrones (via TravelTime, Mapbox, etc.) or has custom boundaries. When geometry is provided, lat/lng are not required.", + "properties": { + "type": { + "type": "string", + "enum": [ + "Polygon", + "MultiPolygon" + ], + "description": "GeoJSON geometry type." + }, + "coordinates": { + "type": "array", + "description": "GeoJSON coordinates array. For Polygon: array of linear rings. For MultiPolygon: array of polygons." + } + }, + "required": [ + "type", + "coordinates" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "oneOf": [ + { + "required": [ + "lat", + "lng", + "travel_time", + "transport_mode" + ], + "not": { + "anyOf": [ + { + "required": [ + "radius" + ] + }, + { + "required": [ + "geometry" + ] + } + ] + } + }, + { + "required": [ + "lat", + "lng", + "radius" + ], + "not": { + "anyOf": [ + { + "required": [ + "travel_time" + ] + }, + { + "required": [ + "geometry" + ] + } + ] + } + }, + { + "required": [ + "geometry" + ], + "not": { + "anyOf": [ + { + "required": [ + "travel_time" + ] + }, + { + "required": [ + "radius" + ] + } + ] + } + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "language": { + "type": "array", + "description": "Restrict to users with specific language preferences. ISO 639-1 codes (e.g., 'en', 'es', 'fr').", + "items": { + "type": "string", + "pattern": "^[a-z]{2}$" + }, + "minItems": 1 + }, + "keyword_targets": { + "type": "array", + "description": "Keyword targeting for search and retail media platforms. Restricts delivery to queries matching the specified keywords. Each keyword is identified by the tuple (keyword, match_type) \u2014 the same keyword string with different match types are distinct targets. Sellers SHOULD reject duplicate (keyword, match_type) pairs within a single request. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "keyword": { + "type": "string", + "minLength": 1, + "description": "The keyword to target" + }, + "match_type": { + "$ref": "#/$defs/MatchType" + }, + "bid_price": { + "type": "number", + "minimum": 0, + "description": "Per-keyword bid price, denominated in the same currency as the package's pricing option. Overrides the package-level bid_price for this keyword. Inherits the max_bid interpretation from the pricing option: when max_bid is true, this is the keyword's bid ceiling; when false, this is the exact bid. If omitted, the package bid_price applies." + } + }, + "required": [ + "keyword", + "match_type" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "negative_keywords": { + "type": "array", + "description": "Keywords to exclude from delivery. Queries matching these keywords will not trigger the ad. Each negative keyword is identified by the tuple (keyword, match_type). Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "keyword": { + "type": "string", + "minLength": 1, + "description": "The keyword to exclude" + }, + "match_type": { + "$ref": "#/$defs/MatchType" + } + }, + "required": [ + "keyword", + "match_type" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "signal_targeting": { + "type": "array", + "description": "DEPRECATED. Use signal_targeting_groups for package-level signal targeting. Legacy flat signal_targeting remains accepted during the SignalRef migration window but cannot express grouped include/exclude composition or product-scoped pricing.", + "deprecated": true, + "items": { + "title": "Signal Targeting", + "description": "Targeting constraint for a specific signal. Uses value_type as discriminator to determine the targeting expression format.", + "discriminator": { + "propertyName": "value_type" + }, + "oneOf": [ + { + "type": "object", + "description": "Binary signal targeting - user either matches or doesn't", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New targeting constraints SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "binary", + "description": "Discriminator for binary signals" + }, + "value": { + "type": "boolean", + "description": "Whether to include (true) or exclude (false) users matching this signal" + } + }, + "required": [ + "value_type", + "value" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Categorical signal targeting - target users with specific values", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New targeting constraints SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "categorical", + "description": "Discriminator for categorical signals" + }, + "values": { + "type": "array", + "description": "Values to target. Users with any of these values will be included.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "value_type", + "values" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Numeric signal targeting - target users within a value range. If min_value is provided, it must be <= max_value. Values should be within the signal's defined range (see signal definition).", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New targeting constraints SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "numeric", + "description": "Discriminator for numeric signals" + }, + "min_value": { + "type": "number", + "description": "Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided. Should be >= signal's range.min if defined." + }, + "max_value": { + "type": "number", + "description": "Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided. Should be <= signal's range.max if defined." + } + }, + "required": [ + "value_type" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "measurement_terms": { + "title": "Measurement Terms", + "description": "Agreed billing measurement and makegood terms for this package. Reflects what was negotiated \u2014 may differ from the buyer's proposal or the product's defaults. When present, these terms are binding for the package's duration.", + "type": "object", + "properties": { + "billing_measurement": { + "type": "object", + "description": "Which vendor's count of the billing metric governs invoicing. The billing metric is determined by the pricing_model on the selected pricing_option (e.g., impressions for CPM, completed views for CPCV).", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor whose measurement of the billing metric is authoritative for invoicing (e.g., { domain: 'campaignmanager.google.com' } for buyer's DCM, { domain: 'admanager.google.com' } for seller's GAM).", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "max_variance_percent": { + "type": "number", + "minimum": 0, + "exclusiveMaximum": 100, + "description": "Maximum acceptable variance between the billing vendor's count and the other party's count before resolution is triggered (e.g., 10 means a 10% divergence triggers review)." + }, + "measurement_window": { + "type": "string", + "description": "Which measurement maturation stage the billing metric is reconciled against. References a window_id from the product's reporting_capabilities.measurement_windows. Examples: 'c7' for broadcast TV guarantees (live + 7 days DVR), 'final' for DOOH after IVT/fraud-check processing, 'post_sivt' for digital after sophisticated invalid-traffic filtering, 'downloads_30d' for podcast. When absent, billing is based on the seller's standard reporting without windowed maturation.", + "examples": [ + "live", + "c3", + "c7", + "tentative", + "final", + "post_ivt", + "post_sivt", + "downloads_30d" + ] + }, + "finalization_deadline_hours": { + "type": "integer", + "minimum": 0, + "description": "Maximum hours by which the authoritative party MUST publish a final record (`is_final: true` / `finalized_at` on `get_media_buy_delivery`, or `final: true` / `finalized_at` on `report_usage`). **Anchor:** when `measurement_window` is set, hours are counted from the close of that window (e.g., 240h after `c7` close = ~10 days after the 7-day DVR accumulation completes); when `measurement_window` is absent, hours are counted from `reporting_period.end`. Picking a single anchor avoids ambiguity for windowed channels where `reporting_period.end` and window close differ by days. The deadline applies to whichever party is named in `vendor` \u2014 seller, buyer, or third-party vendor \u2014 symmetrically. When the deadline elapses without a final record, the counterparty MAY fall back to its own attestation for invoicing (seller falls back to seller-attested numbers via `get_media_buy_delivery`; buyer falls back to a buyer-attested `report_usage` push), and the breach is treated like any other measurement-terms breach under `makegood_policy`. Absent means no contractual deadline \u2014 finalization is best-effort and disagreements resolve out of band." + } + }, + "required": [ + "vendor" + ], + "additionalProperties": true + }, + "makegood_policy": { + "type": "object", + "description": "Remedies available when a performance standard or billing measurement variance is breached. Seller declares which remedy types they support. When a breach occurs, the seller proposes a remedy from this menu; the buyer accepts or disputes.", + "properties": { + "available_remedies": { + "type": "array", + "description": "Remedy types the seller supports. Ordered by seller preference (first = preferred). Seller proposes from this list when a breach occurs; buyer accepts or disputes.", + "items": { + "$ref": "#/$defs/MakegoodRemedy" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "available_remedies" + ], + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "performance_standards": { + "type": "array", + "description": "Agreed performance standards for this package. When any entry specifies a vendor, creatives assigned to this package MUST include corresponding tracker_script or tracker_pixel assets from that vendor.", + "items": { + "title": "Performance Standard", + "description": "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).", + "type": "object", + "properties": { + "metric": { + "$ref": "#/$defs/PerformanceStandardMetric" + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "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." + }, + "standard": { + "$ref": "#/$defs/ViewabilityStandard" + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor measuring this metric (e.g., { domain: 'doubleverify.com' }). The vendor's brand.json agents array (type: 'measurement') is the discovery point for their measurement agent. When specified on a confirmed package, creatives MUST include tracker_script or tracker_pixel assets from this vendor.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + } + }, + "required": [ + "metric", + "threshold", + "vendor" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "committed_metrics": { + "type": "array", + "description": "The binding reporting contract for this package \u2014 what the seller has agreed to populate in delivery reports. Each entry carries an explicit `committed_at` timestamp, so the array also serves as the contract amendment ledger: day-1 commitments share `committed_at = create_media_buy.confirmed_at`; mid-flight additions carry their own timestamps. The `missing_metrics` field on `get_media_buy_delivery` reconciles against this list, filtering to entries where `committed_at < reporting_period.end` (a metric committed mid-flight is only audited from its commitment timestamp forward). Sellers stamp the day-1 set on the `create_media_buy` response; mid-flight additions are appended via `update_media_buy` (append-only \u2014 sellers MUST reject attempts to modify or remove existing entries with `validation_error`, suggested code: `IMMUTABLE_FIELD`). Optional in v1; absence means the seller does not provide an audit-grade contract and `missing_metrics` falls back to the product's live `available_metrics` (a known audit gap \u2014 buyers SHOULD treat absence as 'no audit-grade contract' rather than 'clean delivery'). 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. The unified shape is symmetric with `missing_metrics` and `aggregated_totals.metric_aggregates` \u2014 same atomic unit `(scope, metric_id, qualifier)` across contract, diff, and delivery, so reconciliation collapses to a row-level join on the tuple. Replaces the parallel-array design that shipped briefly in #3510.", + "items": { + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "properties": { + "scope": { + "type": "string", + "const": "standard", + "description": "Standard metric from the closed `available-metric.json` enum." + }, + "metric_id": { + "$ref": "#/$defs/AvailableMetric" + }, + "qualifier": { + "type": "object", + "description": "Disambiguates metrics whose definition varies by qualifier. Today carries five keys \u2014 `viewability_standard` (MRC vs GroupM viewability), `completion_source` (seller- vs vendor-attested completion), `attribution_methodology` (how attribution was computed for outcome metrics), `attribution_window` (the time window over which outcomes were attributed), and `lift_dimension` (which dimension of brand_lift this row represents \u2014 awareness, consideration, etc.). Required when the underlying `metric_id` has multiple incompatible measurement paths AND the seller commits to a specific one. Symmetric on `missing_metrics`. Reserved for additive qualifiers in future minors \u2014 schema is closed (`additionalProperties: false`); new keys ship explicitly. **Heterogeneous value types**: qualifier values can be either string enums (`viewability_standard`, `completion_source`, `attribution_methodology`, `lift_dimension`) or structured objects (`attribution_window` is a duration `{interval, unit}`). Consumers MUST dispatch on key name to know value shape; structured-value qualifiers join on canonical (key-sorted) deep equality so `{interval: 14, unit: 'days'}` and `{unit: 'days', interval: 14}` resolve to the same partition. Rate-style metrics (`new_to_brand_rate`, `engagement_rate`, etc.) inherit the methodology of their numerator \u2014 when a rate carries `attribution_methodology` qualifier, it applies to the underlying conversions/events being rated.", + "properties": { + "viewability_standard": { + "$ref": "#/$defs/ViewabilityStandard" + }, + "completion_source": { + "$ref": "#/$defs/CompletionSource" + }, + "attribution_methodology": { + "$ref": "#/$defs/AttributionMethodology" + }, + "attribution_window": { + "title": "Duration", + "description": "Time window over which outcome attribution is computed. **Object-valued, not string** \u2014 MUST be a structured duration object like `{interval: 14, unit: 'days'}`, NEVER a shorthand string like `'14d'`. SHOULD be set when `metric_id` is an outcome metric and the seller commits to a specific window. Common windows: `{interval: 7, unit: 'days'}`, `{interval: 14, unit: 'days'}`, `{interval: 30, unit: 'days'}`, `{interval: 90, unit: 'days'}`. Two outcome rows with the same `metric_id` and `attribution_methodology` but different `attribution_window` represent the same metric measured over different periods \u2014 the join on `(metric_id, qualifier)` keeps them as separate rows so buyers don't accidentally aggregate across windows.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + }, + "lift_dimension": { + "$ref": "#/$defs/LiftDimension" + } + }, + "additionalProperties": false + }, + "committed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this metric became part of the contract. Day-1 commitments use `create_media_buy.confirmed_at`; mid-flight additions use the time the amendment was accepted." + } + }, + "required": [ + "scope", + "metric_id", + "committed_at" + ], + "additionalProperties": false + }, + { + "properties": { + "scope": { + "type": "string", + "const": "vendor", + "description": "Vendor-defined metric, identified by the tuple `(vendor, metric_id)`." + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. The vendor's `brand.json` `agents[type='measurement']` is the canonical anchor.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary.", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + }, + "committed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this vendor metric became part of the contract." + } + }, + "required": [ + "scope", + "vendor", + "metric_id", + "committed_at" + ], + "additionalProperties": false + } + ] + }, + "minItems": 1, + "examples": [ + [ + { + "scope": "standard", + "metric_id": "impressions", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "standard", + "metric_id": "spend", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "standard", + "metric_id": "completed_views", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "vendor", + "vendor": { + "domain": "attentionvendor.example" + }, + "metric_id": "attention_units", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "standard", + "metric_id": "viewable_rate", + "qualifier": { + "viewability_standard": "mrc" + }, + "committed_at": "2026-05-30T14:22:00Z" + } + ] + ] + }, + "creative_assignments": { + "type": "array", + "description": "Creative assets assigned to this package", + "items": { + "title": "Creative Assignment", + "description": "Assignment of a creative asset to a package with optional placement targeting. Used in create_media_buy and update_media_buy requests. Note: sync_creatives does not support placement_refs or placement_ids - use create/update_media_buy for placement-level targeting.", + "type": "object", + "properties": { + "creative_id": { + "type": "string", + "description": "Unique identifier for the creative", + "x-entity": "creative" + }, + "weight": { + "type": "number", + "description": "Relative delivery weight for this creative (0\u2013100). When multiple creatives are assigned to the same package, weights determine impression distribution proportionally \u2014 a creative with weight 2 gets twice the delivery of weight 1. When omitted, the creative receives equal rotation with other unweighted creatives. A weight of 0 means the creative is assigned but paused (receives no delivery).", + "minimum": 0, + "maximum": 100 + }, + "placement_refs": { + "type": "array", + "description": "Optional array of structured placement references where this creative should run. New senders SHOULD use this field for placement-level targeting because placement IDs are publisher-scoped. When omitted, the creative runs on all buyer-targetable placements in the package. References entries from the product's `placements[]` array by `{ publisher_domain, placement_id }`; if `publisher_domain` is omitted in the ref, receivers MAY interpret it relative to the seller agent's own publisher domain in legacy single-publisher contexts. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`.", + "items": { + "title": "Placement Reference", + "description": "Reference to a placement by publisher domain and placement ID. Placement IDs are publisher-scoped, matching the placement catalog in that publisher's adagents.json. When `publisher_domain` is omitted on legacy inputs, receivers MAY interpret the placement ID relative to the seller agent's own publisher domain; new senders SHOULD include `publisher_domain`.", + "type": "object", + "properties": { + "publisher_domain": { + "type": "string", + "description": "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.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "placement_id": { + "type": "string", + "description": "Placement ID from the publisher's adagents.json placement catalog, or an inline seller-defined placement ID interpreted within the same publisher namespace." + } + }, + "required": [ + "placement_id" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "placement_ids": { + "type": "array", + "description": "Legacy shorthand array of placement IDs where this creative should run. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. When omitted, the creative runs on all buyer-targetable placements in the package. Receivers MAY interpret string IDs relative to the seller agent's own publisher domain in legacy single-publisher contexts. If `placement_refs` is also present, receivers MUST ignore this field.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "creative_id" + ], + "additionalProperties": true + } + }, + "format_ids_to_provide": { + "type": "array", + "description": "Format IDs that creative assets will be provided for this package", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + } + }, + "optimization_goals": { + "type": "array", + "description": "Optimization targets for this package. The seller optimizes delivery toward these goals in priority order. Common pattern: event goals (purchase, install) as primary targets at priority 1; metric goals (clicks, views) as secondary proxy signals at priority 2+.", + "items": { + "title": "Optimization Goal", + "description": "A single optimization target for a package. Packages accept an array of optimization_goals. When multiple goals are present, priority determines which the seller focuses on \u2014 1 is highest priority (primary goal); higher numbers are secondary. Duplicate priority values result in undefined seller behavior.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Optimize for a seller-tracked delivery metric. No event source required \u2014 the seller tracks these natively.", + "properties": { + "kind": { + "type": "string", + "const": "metric" + }, + "metric": { + "type": "string", + "enum": [ + "clicks", + "views", + "completed_views", + "viewed_seconds", + "attention_seconds", + "attention_score", + "engagements", + "follows", + "saves", + "profile_visits", + "reach" + ], + "description": "Seller-native metric to optimize for. Delivery metrics: clicks (link clicks, swipe-throughs, CTA taps that navigate away), views (viewable impressions), completed_views (video/audio completions \u2014 see view_duration_seconds), reach (unique audience reach \u2014 see reach_unit and target_frequency). Duration/score metrics: viewed_seconds (time in view per impression \u2014 reported back via `delivery-metrics.viewability.viewed_seconds`, governed by the viewability `standard`). Audience action metrics: engagements (any direct interaction with the ad unit beyond viewing \u2014 social reactions/comments/shares, story/unit opens, interactive overlay taps, companion banner interactions on audio and CTV), follows (new followers, page likes, artist/podcast/channel subscribes), saves (saves, bookmarks, playlist adds, pins \u2014 signals of intent to return), profile_visits (visits to the brand's in-platform page \u2014 profile, artist page, channel, or storefront. Does not include external website clicks, which are covered by 'clicks'). **DEPRECATED values** (slated for removal at next major): `attention_seconds` and `attention_score` \u2014 these have no industry-graduated definition (DoubleVerify, IAS, Adelaide, TVision, Lumen each define them differently) and cannot be meaningfully optimized for without a vendor binding. Use `kind: 'vendor_metric'` with an explicit `vendor` and `metric_id` instead \u2014 that path binds the goal to a specific measurement vendor and reconciles to the same `(vendor, metric_id)` key in delivery's `vendor_metric_values[]`. Sellers MAY reject the deprecated values with `TERMS_REJECTED` and a suggestion to use the `vendor_metric` kind." + }, + "reach_unit": { + "allOf": [ + { + "$ref": "#/$defs/ReachUnit" + } + ], + "description": "Unit for reach measurement. Required when metric is 'reach'. Must be a value declared in the product's metric_optimization.supported_reach_units." + }, + "target_frequency": { + "type": "object", + "description": "Target frequency band for reach optimization. Only applicable when metric is 'reach'. Frames frequency as an optimization signal: the seller should treat impressions toward entities already within the [min, max] band as lower-value, and impressions toward unreached entities as higher-value. This shifts budget toward fresh reach rather than re-reaching known users. When omitted, the seller maximizes unique reach without a frequency constraint. A hard cap can still be layered via targeting_overlay.frequency_cap if a ceiling is needed.", + "properties": { + "min": { + "type": "integer", + "minimum": 1, + "description": "Minimum frequency for an entity to be considered meaningfully reached within the window. Impressions that would bring an entity below this threshold are treated as high-value (growing reach). When omitted, the seller uses their platform default (typically 1)." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Frequency at which an entity is considered saturated within the window. Impressions toward entities at or above this threshold are treated as lower-value. When both min and max are present, max must be greater than or equal to min. When omitted, the seller determines the saturation point." + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Time window over which frequency is measured (e.g. {\"interval\": 7, \"unit\": \"days\"} or {\"interval\": 1, \"unit\": \"campaign\"} for the full flight). Weekly windows are typical for brand campaigns; daily windows suit high-cadence direct response." + } + }, + "required": [ + "window" + ], + "anyOf": [ + { + "required": [ + "min" + ] + }, + { + "required": [ + "max" + ] + } + ], + "additionalProperties": true + }, + "view_duration_seconds": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Minimum video view duration in seconds that qualifies as a completed_view for this goal. Only applicable when metric is 'completed_views'. When omitted, the seller uses their platform default (typically 2\u201315 seconds). Common values: 2 (Snap/LinkedIn default), 6 (TikTok), 15 (Snap 15-second views, Meta ThruPlay). Sellers declare which durations they support in metric_optimization.supported_view_durations. Sellers must reject goals with unsupported values \u2014 silent rounding would create measurement discrepancies." + }, + "target": { + "description": "Target for this metric. When omitted, the seller optimizes for maximum metric volume within budget.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Target cost per unit of the metric (e.g., cost per click, cost per completed view).", + "properties": { + "kind": { + "type": "string", + "const": "cost_per" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target cost per metric unit in the buy currency" + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Minimum per-impression rate for this metric. The metric defines the units: proportions for count metrics (e.g., 0.001 for 0.1% CTR, 0.70 for 70% viewability), seconds for duration metrics (e.g., 3.0 for 3s in view), or score for score metrics.", + "properties": { + "kind": { + "type": "string", + "const": "threshold_rate" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Minimum per-impression value. Units depend on the metric: proportion (clicks, views, completed_views), seconds (viewed_seconds, attention_seconds), or score (attention_score)." + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + } + ] + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Relative priority among all optimization goals on this package. 1 = highest priority (primary goal); higher numbers are lower priority (secondary signals). When omitted, sellers may use array position as priority." + } + }, + "required": [ + "kind", + "metric" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Optimize for advertiser-tracked conversion events. Requires event sources registered via sync_event_sources.", + "properties": { + "kind": { + "type": "string", + "const": "event" + }, + "event_sources": { + "type": "array", + "description": "Event source and type pairs that feed this goal. Each entry identifies a source and event type to include. When the seller supports multi_source_event_dedup (declared in get_adcp_capabilities), they deduplicate by event_id across all entries \u2014 the same business event from multiple sources counts once, using value_field and value_factor from the first matching entry. When multi_source_event_dedup is false or absent, buyers should use a single entry per goal; the seller will use only the first entry. All event sources must be configured via sync_event_sources.", + "items": { + "type": "object", + "properties": { + "event_source_id": { + "type": "string", + "minLength": 1, + "description": "Event source to include (must be configured on this account via sync_event_sources)" + }, + "event_type": { + "$ref": "#/$defs/EventType" + }, + "custom_event_name": { + "type": "string", + "description": "Required when event_type is 'custom'. Platform-specific name for the custom event." + }, + "value_field": { + "type": "string", + "description": "Which field in the event's custom_data carries the monetary value. The seller must use this field for value extraction and aggregation when computing ROAS and conversion value metrics. Required on at least one entry when target.kind is 'per_ad_spend' or 'maximize_value' \u2014 sellers must reject these target kinds when no event source entry includes value_field. When present without a value-oriented target, the seller may use it for delivery reporting (conversion_value, roas) but must not change the optimization objective. Common values: 'value', 'order_total', 'profit_margin'. This is not passed as a parameter to underlying platform APIs \u2014 the seller maps it to their platform's value ingestion mechanism." + }, + "value_factor": { + "type": "number", + "default": 1, + "description": "Multiplier the seller must apply to value_field before aggregation. Use -1 for refund events (negate the value), 0.01 for values in cents, -0.01 for refunds in cents. A value of 0 zeroes out this source's value contribution (the source still counts for event dedup). Defaults to 1. This is not passed as a parameter to underlying platform APIs \u2014 the seller applies it when computing aggregated value metrics." + } + }, + "required": [ + "event_source_id", + "event_type" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "target": { + "description": "Target cost or return for this event goal. When omitted, the seller optimizes for maximum conversion count within budget \u2014 regardless of whether value_field is present on event sources. The presence of value_field alone does not change the optimization objective; it only makes value available for reporting. An explicit target of maximize_value or per_ad_spend is required to steer toward value.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Target cost per conversion event (after deduplication across event_sources).", + "properties": { + "kind": { + "type": "string", + "const": "cost_per" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target cost per event in the buy currency" + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Target return per unit of ad spend, calculated as sum(value_field * value_factor) / spend across all deduplicated events. Requires value_field on at least one event_sources entry.", + "properties": { + "kind": { + "type": "string", + "const": "per_ad_spend" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target return ratio (e.g., 4.0 means $4 of value per $1 spent)" + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Maximize total conversion value within budget, without a specific return ratio target. Steers spend toward higher-value conversions rather than maximizing conversion count. Requires value_field on at least one event_sources entry.", + "properties": { + "kind": { + "type": "string", + "const": "maximize_value" + } + }, + "required": [ + "kind" + ], + "additionalProperties": true + } + ] + }, + "attribution_window": { + "allOf": [ + { + "title": "Attribution Window", + "description": "Describes the attribution methodology and lookback windows used for conversion measurement. Enables cross-platform comparison by making attribution methodology transparent. Used as a `$ref` from `optimization-goal.json` (buyer's optimization-time attribution choice), `get-media-buy-delivery-response.json` (seller-declared attribution methodology in delivery reports), and similar surfaces. All fields are optional individually but at least one of `post_click`, `post_view`, or `model` SHOULD be populated; absence of `model` means the seller's default attribution model applies (typically `last_touch` per industry convention) \u2014 sellers SHOULD populate `model` explicitly when committing to a specific methodology.", + "type": "object", + "properties": { + "post_click": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Post-click attribution window. Conversions occurring within this duration after a click are attributed to the ad." + }, + "post_view": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Post-view attribution window. Conversions occurring within this duration after an ad impression (without click) are attributed to the ad." + }, + "model": { + "$ref": "#/$defs/AttributionModel" + } + }, + "additionalProperties": true + } + ], + "description": "Attribution window for this optimization goal \u2014 references the canonical `attribution-window` shape (post_click, post_view, model). Values must match an option declared in the seller's `conversion_tracking.attribution_windows` capability. Sellers MUST reject windows not in their declared capabilities. When the entire field is omitted, the seller uses their default window." + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Relative priority among all optimization goals on this package. 1 = highest priority (primary goal); higher numbers are lower priority (secondary signals). When omitted, sellers may use array position as priority." + } + }, + "required": [ + "kind", + "event_sources" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Optimize for a vendor-attested measurement metric. Use when the metric has no graduated industry-standard definition and must be reconciled to a specific vendor \u2014 e.g., attention (DoubleVerify, IAS, Adelaide, TVision, Lumen), panel-based brand lift (Kantar, Upwave, Cint), emissions (Scope3, Good-Loop), retail-media partner metrics. The vendor + metric_id pair binds buyer\u2192seller\u2192vendor end-to-end: the seller's bidding stack steers toward this specific vendor's measurement, and delivery reports the value via `vendor_metric_values[]` with the same `(vendor, metric_id)` key. Three preconditions for goal acceptance: (1) Discovery \u2014 the `metric_id` SHOULD appear in the vendor's published `measurement.metrics[]` catalog (queried from the vendor's `brand.json` `agents[type='measurement']`). Sellers SHOULD verify against a cached snapshot of the vendor's capability response; staleness handling is implementation-defined. SHOULD this minor while measurement-vendor adoption of AdCP-conformant capability publication catches up; tightens to MUST at the next minor. (2) Capability \u2014 the `(vendor, metric_id)` pair MUST appear in the product's `vendor_metric_optimization.supported_metrics[]`, and the goal's `target.kind` MUST appear in that entry's `supported_targets`. (3) Reporting coherence \u2014 the package's `committed_metrics[]` MUST include a matching `{ scope: 'vendor', vendor, metric_id }` entry. Sellers MUST reject goals failing the capability or reporting-coherence preconditions. Optimization without committed reporting is unverifiable and is therefore disallowed at the wire level. Precondition checks are seller-runtime; the schema's `required` only validates structural presence of `kind`, `vendor`, and `metric_id`.", + "properties": { + "kind": { + "type": "string", + "const": "vendor_metric" + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. Same shape as `vendor_metric_values.vendor`, `reporting_capabilities.vendor_metrics[].vendor`, and `vendor_metric_optimization.supported_metrics[].vendor` \u2014 symmetric across discovery, capability, commitment, optimization, and reporting surfaces.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary (e.g., `attention_score`, `attention_seconds`, `gco2e_per_impression`, `awareness_lift`). MUST be present in the vendor's published `measurement.metrics[]` catalog and in the product's `vendor_metric_optimization.supported_metrics[]`.", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + }, + "target": { + "description": "Target for this vendor metric. When omitted, the seller optimizes for maximum metric volume / score within budget. `cost_per` and `threshold_rate` semantics mirror the same target kinds on the `metric` kind \u2014 units are vendor-defined and depend on the vendor's `measurement.metrics[]` declaration for this `metric_id`.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Target cost per unit of the vendor metric (e.g., cost per attention-second).", + "properties": { + "kind": { + "type": "string", + "const": "cost_per" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target cost per metric unit in the buy currency. Units of the metric are vendor-defined." + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Minimum per-impression value for this vendor metric (e.g., attention_score \u2265 70).", + "properties": { + "kind": { + "type": "string", + "const": "threshold_rate" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Minimum per-impression value. Units of the metric are vendor-defined." + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + } + ] + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Relative priority among all optimization goals on this package. 1 = highest priority (primary goal); higher numbers are lower priority (secondary signals). When omitted, sellers may use array position as priority." + } + }, + "required": [ + "kind", + "vendor", + "metric_id" + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + }, + "start_time": { + "type": "string", + "format": "date-time", + "not": { + "const": "asap" + }, + "description": "Flight start date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's start_time. Sellers SHOULD always include the resolved value in responses, even when inherited." + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Flight end date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's end_time. Sellers SHOULD always include the resolved value in responses, even when inherited." + }, + "paused": { + "type": "boolean", + "description": "Whether this package is paused by the buyer. Paused packages do not deliver impressions. Defaults to false.", + "default": false + }, + "canceled": { + "type": "boolean", + "description": "Whether this package has been canceled. Canceled packages stop delivery and cannot be reactivated. Defaults to false.", + "default": false + }, + "cancellation": { + "type": "object", + "description": "Cancellation metadata. Present only when canceled is true.", + "required": [ + "canceled_at", + "canceled_by" + ], + "properties": { + "canceled_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this package was canceled." + }, + "canceled_by": { + "$ref": "#/$defs/CanceledBy" + }, + "reason": { + "type": "string", + "description": "Reason the package was canceled.", + "maxLength": 500 + }, + "acknowledged_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the seller acknowledged the cancellation. Confirms inventory has been released and billing stopped. Absent until the seller processes the cancellation." + } + }, + "additionalProperties": false + }, + "agency_estimate_number": { + "type": "string", + "maxLength": 100, + "description": "Agency estimate or authorization number for this package. Echoed from the buyer's request. When present on the package, takes precedence over the media buy-level estimate number." + }, + "creative_deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp for creative upload or change deadline for this package. After this deadline, creative changes are rejected. When absent, the media buy's creative_deadline applies." + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "package_id" + ], + "additionalProperties": true + } + }, + "planned_delivery": { + "title": "Planned Delivery", + "description": "The seller's interpreted delivery parameters. Describes what the seller will actually run -- geo, channels, flight dates, frequency caps, and budget. Present when the account has governance_agents or when the seller chooses to provide delivery transparency.", + "type": "object", + "properties": { + "geo": { + "type": "object", + "description": "Geographic targeting the seller will apply.", + "properties": { + "countries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-1 alpha-2 country codes where ads will deliver." + }, + "regions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-2 subdivision codes where ads will deliver." + } + }, + "additionalProperties": true + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/MediaChannel" + }, + "description": "Channels the seller will deliver on." + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "Actual flight start the seller will use." + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Actual flight end the seller will use." + }, + "frequency_cap": { + "title": "Frequency Cap", + "description": "Frequency cap the seller will apply.", + "type": "object", + "properties": { + "suppress": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Cooldown period between consecutive exposures to the same entity. Prevents back-to-back ad delivery (e.g. {\"interval\": 60, \"unit\": \"minutes\"} for a 1-hour cooldown). Preferred over suppress_minutes." + }, + "suppress_minutes": { + "type": "number", + "description": "Deprecated \u2014 use suppress instead. Cooldown period in minutes between consecutive exposures to the same entity (e.g. 60 for a 1-hour cooldown).", + "minimum": 0 + }, + "max_impressions": { + "type": "integer", + "description": "Maximum number of impressions per entity per window. For duration windows, implementations typically use a rolling window; 'campaign' applies a fixed cap across the full flight.", + "minimum": 1 + }, + "per": { + "allOf": [ + { + "$ref": "#/$defs/ReachUnit" + } + ], + "description": "Entity granularity for impression counting. Required when max_impressions is set." + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Time window for the max_impressions cap (e.g. {\"interval\": 7, \"unit\": \"days\"} or {\"interval\": 1, \"unit\": \"campaign\"} for the full flight). Required when max_impressions is set." + } + }, + "anyOf": [ + { + "required": [ + "suppress" + ] + }, + { + "required": [ + "suppress_minutes" + ] + }, + { + "required": [ + "max_impressions" + ] + } + ], + "dependencies": { + "max_impressions": [ + "per", + "window" + ], + "per": [ + "max_impressions" + ], + "window": [ + "max_impressions" + ] + }, + "additionalProperties": true + }, + "audience_summary": { + "type": "string", + "description": "Human-readable summary of the audience the seller will target." + }, + "audience_targeting": { + "type": "array", + "description": "Structured audience targeting the seller will activate. Each entry is either a signal reference or a descriptive criterion. When present, governance agents MUST use this for bias/fairness validation and SHOULD ignore audience_summary for validation purposes. The audience_summary field is a human-readable rendering of this array, not an independent declaration.", + "items": { + "title": "Audience Selector", + "description": "Selects an audience by signal reference or natural language description. Uses 'type' as the primary discriminator (signal vs description). Signal selectors additionally use 'value_type' to determine the targeting expression format (matching signal-targeting.json variants).", + "oneOf": [ + { + "type": "object", + "description": "Signal-based selector for binary signals \u2014 user either matches or doesn't. The governance agent can resolve catalog signals to their full definition for structural validation.", + "properties": { + "type": { + "type": "string", + "const": "signal", + "description": "Discriminator for signal-based selectors" + }, + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New selectors SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "binary", + "description": "Discriminator for binary signals" + }, + "value": { + "type": "boolean", + "description": "Whether to include (true) or exclude (false) users matching this signal" + } + }, + "required": [ + "type", + "value_type", + "value" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-based selector for categorical signals \u2014 target users with specific values.", + "properties": { + "type": { + "type": "string", + "const": "signal", + "description": "Discriminator for signal-based selectors" + }, + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New selectors SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "categorical", + "description": "Discriminator for categorical signals" + }, + "values": { + "type": "array", + "description": "Values to target. Users with any of these values will be included.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "type", + "value_type", + "values" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-based selector for numeric signals \u2014 target users within a value range.", + "properties": { + "type": { + "type": "string", + "const": "signal", + "description": "Discriminator for signal-based selectors" + }, + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New selectors SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "numeric", + "description": "Discriminator for numeric signals" + }, + "min_value": { + "type": "number", + "description": "Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided." + }, + "max_value": { + "type": "number", + "description": "Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided." + } + }, + "required": [ + "type", + "value_type" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Description-based selector \u2014 natural language audience description. Used when the caller doesn't have a specific signal reference (e.g., buyer setting plan-level goals, or seller using proprietary segments without public catalog entries).", + "properties": { + "type": { + "type": "string", + "const": "description", + "description": "Discriminator for description-based selectors" + }, + "description": { + "type": "string", + "description": "Natural language description of the audience (e.g., 'likely EV buyers', 'high net worth individuals', 'vulnerable communities')", + "minLength": 1, + "maxLength": 2000 + }, + "category": { + "type": "string", + "description": "Optional grouping hint for the governance agent (e.g., 'demographic', 'behavioral', 'contextual', 'financial')" + } + }, + "required": [ + "type", + "description" + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + }, + "total_budget": { + "type": "number", + "description": "Total budget the seller will deliver against.", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for the budget.", + "pattern": "^[A-Z]{3}$" + }, + "enforced_policies": { + "type": "array", + "description": "Registry policy IDs the seller will enforce for this delivery.", + "items": { + "type": "string" + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "sandbox": { + "type": "boolean", + "description": "When true, this response contains simulated data from sandbox mode." + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "media_buy_id", + "packages" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "CreateMediaBuyError", + "description": "Error response - operation failed, no media buy created", + "type": "object", + "properties": { + "errors": { + "type": "array", + "description": "Array of errors explaining why the operation failed", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "media_buy_id" + ] + }, + { + "required": [ + "packages" + ] + }, + { + "required": [ + "sandbox" + ] + }, + { + "properties": { + "status": { + "const": "submitted" + } + }, + "required": [ + "status" + ] + } + ] + } + }, + { + "title": "CreateMediaBuySubmitted", + "description": "Async task envelope returned when the media buy cannot be confirmed before the response is emitted \u2014 for example, when a guaranteed buy requires IO signing, when governance review is outstanding, or when the seller has queued the request for batch processing. The buyer polls tasks/get with task_id or receives a webhook when the task completes; the media_buy_id and packages land on the completion artifact, not this envelope. Do not use a 'pending_approval' MediaBuy.status for this case \u2014 that value is not in MediaBuyStatus; IO review and similar pre-issuance workflows are modeled at the task layer only.", + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "submitted", + "description": "Task-level status literal. Discriminates this async envelope from the synchronous success shape, whose status field carries a MediaBuyStatus value (pending_creatives, pending_start, active). See task-status.json for the full task-status enum." + }, + "task_id": { + "type": "string", + "description": "Task handle the buyer uses with tasks/get, and that the seller references on push-notification callbacks. The media_buy_id is issued on the completion artifact, not here. Per AdCP wire conventions this is snake_case; A2A adapters MAY surface it as taskId, but the payload field emitted by the agent is task_id.", + "x-entity": "task" + }, + "message": { + "type": "string", + "maxLength": 2000, + "description": "Optional human-readable explanation of why the task is submitted \u2014 e.g., 'Awaiting IO signature from sales team; typical turnaround 2\u20134 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context \u2014 a hostile seller may inject prompt-injection payloads aimed at the buyer's agent." + }, + "errors": { + "type": "array", + "description": "Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "status", + "task_id" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "media_buy_id" + ] + }, + { + "required": [ + "packages" + ] + } + ] + } + } + ], + "properties": {} + }, + { + "title": "Create Media Buy - Working", + "description": "Progress payload for active create_media_buy task.", + "type": "object", + "properties": { + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Completion percentage (0-100)" + }, + "current_step": { + "type": "string", + "description": "Current step or phase of the operation" + }, + "total_steps": { + "type": "integer", + "minimum": 1, + "description": "Total number of steps in the operation" + }, + "step_number": { + "type": "integer", + "minimum": 1, + "description": "Current step number" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + { + "title": "Create Media Buy - Input Required", + "description": "Payload when task is paused waiting for user input or approval.", + "type": "object", + "properties": { + "reason": { + "type": "string", + "enum": [ + "APPROVAL_REQUIRED", + "BUDGET_EXCEEDS_LIMIT" + ], + "description": "Reason code indicating why input is needed" + }, + "errors": { + "type": "array", + "description": "Optional validation errors or warnings for debugging purposes. Helps explain why input is required.", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + { + "title": "Create Media Buy - Submitted", + "description": "Async task envelope returned when create_media_buy cannot be confirmed before the response is emitted. The buyer polls tasks/get with task_id or receives a webhook when the task completes; the media_buy_id and packages land on the completion artifact, not this envelope.", + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "submitted", + "description": "Task-level status literal. Discriminates this async envelope from the synchronous success shape, whose status field carries a MediaBuyStatus value (pending_creatives, pending_start, active). See task-status.json for the full task-status enum." + }, + "task_id": { + "type": "string", + "description": "Task handle the buyer uses with tasks/get, and that the seller references on push-notification callbacks. The media_buy_id is issued on the completion artifact, not here. Per AdCP wire conventions this is snake_case; A2A adapters MAY surface it as taskId, but the payload field emitted by the agent is task_id.", + "x-entity": "task" + }, + "message": { + "type": "string", + "maxLength": 2000, + "description": "Optional human-readable explanation of why the task is submitted \u2014 e.g., 'Awaiting IO signature from sales team; typical turnaround 2\u20134 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context \u2014 a hostile seller may inject prompt-injection payloads aimed at the buyer's agent." + }, + "errors": { + "type": "array", + "description": "Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "status", + "task_id" + ], + "additionalProperties": true + }, + { + "title": "Update Media Buy Response", + "description": "Response payload for update_media_buy task. Exactly one of three shapes: (1) synchronous success \u2014 media_buy_id and updated state are issued in-line; (2) terminal failure \u2014 an errors array with no changes applied; (3) submitted task envelope \u2014 status 'submitted' with task_id when the update is queued for async processing (e.g., awaiting operator re-approval for mid-flight changes). The submitted branch MAY carry advisory errors for non-blocking warnings; terminal failures belong in the error branch. These three shapes are mutually exclusive \u2014 a response has exactly one.", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "$ref": "#/$defs/AuthenticationScheme" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "title": "UpdateMediaBuySuccess", + "description": "Success response - media buy updated successfully", + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Seller's identifier for the media buy", + "x-entity": "media_buy" + }, + "media_buy_status": { + "$ref": "#/$defs/MediaBuyStatus" + }, + "status": { + "$ref": "#/$defs/MediaBuyStatus" + }, + "revision": { + "type": "integer", + "description": "Revision number after this update. Use this value in subsequent update_media_buy requests for optimistic concurrency.", + "minimum": 1 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code for monetary values at this media buy level. Echoed when the update affects budget or currency. Matches the currency field in subsequent get_media_buys responses.", + "pattern": "^[A-Z]{3}$" + }, + "total_budget": { + "type": "number", + "description": "Updated total budget amount across all packages, denominated in currency. Echoed when the update affects package budgets so buyers can verify the new aggregate without a round-trip to get_media_buys. Matches the total_budget field in subsequent get_media_buys responses.", + "minimum": 0 + }, + "implementation_date": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "ISO 8601 timestamp when changes take effect (null if pending approval)" + }, + "invoice_recipient": { + "title": "Business Entity", + "description": "Updated invoice recipient, echoed from the request when provided. Confirms the seller accepted the billing override. Bank details are omitted (write-only).", + "type": "object", + "properties": { + "legal_name": { + "type": "string", + "description": "Registered legal name of the business entity", + "maxLength": 200 + }, + "vat_id": { + "type": "string", + "description": "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.", + "pattern": "^[A-Z]{2}[A-Z0-9]{2,13}$" + }, + "tax_id": { + "type": "string", + "description": "Tax identification number for jurisdictions that do not use VAT (e.g., US EIN)", + "maxLength": 30 + }, + "registration_number": { + "type": "string", + "description": "Company registration number (e.g., HRB 12345 for German Handelsregister)", + "maxLength": 50 + }, + "address": { + "type": "object", + "description": "Postal address for invoicing and legal correspondence", + "properties": { + "street": { + "type": "string", + "description": "Street address including building number", + "maxLength": 200 + }, + "city": { + "type": "string", + "maxLength": 100 + }, + "postal_code": { + "type": "string", + "maxLength": 20 + }, + "region": { + "type": "string", + "description": "State, province, or region", + "maxLength": 100 + }, + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code", + "pattern": "^[A-Z]{2}$" + } + }, + "required": [ + "street", + "city", + "postal_code", + "country" + ], + "additionalProperties": false + }, + "contacts": { + "type": "array", + "description": "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.", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "billing", + "legal", + "creative", + "general" + ], + "enumDescriptions": { + "billing": "Accounts payable and invoice queries", + "legal": "Contract and compliance matters", + "creative": "Material submission and creative approval", + "general": "Default contact when no specific role applies" + }, + "description": "Contact's functional role in the business relationship" + }, + "name": { + "type": "string", + "description": "Full name of the contact", + "maxLength": 200 + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 254 + }, + "phone": { + "type": "string", + "maxLength": 30 + } + }, + "required": [ + "role" + ], + "additionalProperties": false + }, + "maxItems": 10 + }, + "bank": { + "type": "object", + "writeOnly": true, + "description": "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.", + "properties": { + "account_holder": { + "type": "string", + "description": "Name on the bank account", + "maxLength": 200 + }, + "iban": { + "type": "string", + "description": "International Bank Account Number (SEPA markets)", + "pattern": "^[A-Z]{2}[0-9]{2}[A-Z0-9]{4,30}$" + }, + "bic": { + "type": "string", + "description": "Bank Identifier Code / SWIFT code (SEPA markets)", + "pattern": "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$" + }, + "routing_number": { + "type": "string", + "description": "Bank routing number for non-SEPA markets (e.g., US ABA routing number, Canadian transit/institution number)", + "maxLength": 30 + }, + "account_number": { + "type": "string", + "description": "Bank account number for non-SEPA markets", + "maxLength": 30 + } + }, + "required": [ + "account_holder" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "legal_name" + ], + "additionalProperties": false, + "examples": [ + { + "description": "German agency with full B2B details", + "data": { + "legal_name": "Pinnacle Media GmbH", + "vat_id": "DE123456789", + "registration_number": "HRB 12345", + "address": { + "street": "Friedrichstrasse 100", + "city": "Berlin", + "postal_code": "10117", + "country": "DE" + }, + "contacts": [ + { + "role": "billing", + "name": "Sam Adeyemi", + "email": "billing@pinnacle-media.com", + "phone": "+49 30 12345678" + } + ], + "bank": { + "account_holder": "Pinnacle Media GmbH", + "iban": "DE89370400440532013000", + "bic": "COBADEFFXXX" + } + } + }, + { + "description": "US advertiser with EIN and domestic bank details", + "data": { + "legal_name": "Acme Corporation", + "tax_id": "12-3456789", + "address": { + "street": "123 Main St", + "city": "New York", + "postal_code": "10001", + "region": "NY", + "country": "US" + }, + "contacts": [ + { + "role": "billing", + "name": "AP Department", + "email": "ap@acme-corp.com" + } + ], + "bank": { + "account_holder": "Acme Corporation", + "routing_number": "021000021", + "account_number": "123456789" + } + } + } + ] + }, + "affected_packages": { + "type": "array", + "description": "Array of packages that were modified with complete state information", + "items": { + "title": "Package", + "description": "A specific product within a media buy (line item)", + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Seller's unique identifier for the package", + "x-entity": "package" + }, + "product_id": { + "type": "string", + "description": "ID of the product this package is based on", + "x-entity": "product" + }, + "budget": { + "type": "number", + "description": "Budget allocation for this package in the currency specified by the pricing option", + "minimum": 0 + }, + "pacing": { + "$ref": "#/$defs/Pacing" + }, + "pricing_option_id": { + "type": "string", + "description": "ID of the selected pricing option from the product's pricing_options array", + "x-entity": "product_pricing_option" + }, + "bid_price": { + "type": "number", + "description": "Bid price for auction-based pricing. This is the exact bid/price to honor unless the selected pricing option has max_bid=true, in which case bid_price is the buyer's maximum willingness to pay (ceiling).", + "minimum": 0 + }, + "price_breakdown": { + "description": "Breaks down the composition of fixed_price from a list (rate card) price through adjustments. Adjustments fall into four kinds: fees (increase buyer price), discounts (reduce buyer price), commissions (revenue splits that don't affect buyer price), and settlement terms (applied at invoicing). The invariant is: list_price with all fee and discount adjustments applied sequentially equals fixed_price. Fees increase the running price; discounts reduce it. This invariant applies only when fixed_price is present on the parent object; on auction-based packages the breakdown is informational only. All monetary values are rounded to currency precision at each step. Budgets are always denominated at the fixed_price level, inclusive of commissions.", + "title": "Price Breakdown", + "type": "object", + "properties": { + "list_price": { + "type": "number", + "description": "Rate card or base price before any adjustments. The starting point from which fixed_price is derived by applying fee and discount adjustments sequentially.", + "exclusiveMinimum": 0 + }, + "adjustments": { + "type": "array", + "description": "Ordered list of price adjustments. Fee and discount adjustments walk list_price to fixed_price \u2014 fees increase the running price, discounts reduce it. Commission and settlement adjustments are disclosed for transparency but do not affect the buyer's committed price.", + "items": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/PriceAdjustmentKind" + }, + "name": { + "type": "string", + "description": "Specific adjustment name. Use well-known values where applicable for interoperability.", + "maxLength": 64, + "examples": [ + "ad_serving", + "data_targeting", + "brand_safety", + "volume", + "negotiated", + "early_booking", + "agency", + "intermediary", + "cash_discount", + "early_payment" + ] + }, + "rate": { + "type": "number", + "description": "Adjustment as a decimal proportion (e.g., 0.15 for 15%). Always positive \u2014 kind determines the economic effect. Mutually exclusive with amount.", + "exclusiveMinimum": 0, + "exclusiveMaximum": 1 + }, + "amount": { + "type": "number", + "description": "Adjustment as a fixed monetary amount in the pricing option's currency. Always positive \u2014 kind determines the economic effect. Mutually exclusive with rate.", + "exclusiveMinimum": 0 + }, + "description": { + "type": "string", + "description": "Human-readable description of this adjustment (e.g., 'Malstaffel 12x', '2% Skonto 10 Tage')", + "maxLength": 256 + }, + "beneficiary": { + "type": "string", + "description": "Identifies who receives this adjustment's value. For commissions, the intermediary (e.g., a sellers.json domain, an AdCP account ID, or a human-readable party name). Optional but recommended for multi-intermediary transparency.", + "maxLength": 256 + } + }, + "required": [ + "kind", + "name" + ], + "oneOf": [ + { + "required": [ + "rate" + ] + }, + { + "required": [ + "amount" + ] + } + ], + "additionalProperties": true + }, + "minItems": 1, + "maxItems": 20 + } + }, + "required": [ + "list_price", + "adjustments" + ], + "additionalProperties": true + }, + "impressions": { + "type": "number", + "description": "Impression goal for this package", + "minimum": 0 + }, + "catalogs": { + "type": "array", + "description": "Catalogs this package promotes. Each catalog MUST have a distinct type (e.g., one product catalog, one store catalog). This constraint is enforced at the application level \u2014 sellers MUST reject requests containing multiple catalogs of the same type with a validation_error. Echoed from the create_media_buy request.", + "items": { + "title": "Catalog", + "description": "A typed data feed. Catalogs carry the items, locations, stock levels, or pricing that publishers use to render ads. They can be synced to a platform via sync_catalogs (managed lifecycle with approval), provided inline, or fetched from an external URL. The catalog type determines the item schema and can be structural (offering, product, inventory, store, promotion) or vertical-specific (hotel, flight, job, vehicle, real_estate, education, destination, app). Selectors (ids, tags, category, query) filter items regardless of sourcing method.", + "type": "object", + "properties": { + "catalog_id": { + "type": "string", + "description": "Buyer's identifier for this catalog. Required when syncing via sync_catalogs. When used in creatives, references a previously synced catalog on the account.", + "x-entity": "catalog" + }, + "name": { + "type": "string", + "description": "Human-readable name for this catalog (e.g., 'Summer Products 2025', 'Amsterdam Store Locations')." + }, + "type": { + "$ref": "#/$defs/CatalogType" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to an external catalog feed. The platform fetches and resolves items from this URL. For offering-type catalogs, the feed contains an array of Offering objects. For other types, the feed format is determined by feed_format. When omitted with type 'product', the platform uses its synced copy of the brand's product catalog." + }, + "feed_format": { + "$ref": "#/$defs/FeedFormat" + }, + "update_frequency": { + "$ref": "#/$defs/UpdateFrequency" + }, + "items": { + "type": "array", + "description": "Inline catalog data. The item schema depends on the catalog type: Offering objects for 'offering', StoreItem for 'store', HotelItem for 'hotel', FlightItem for 'flight', JobItem for 'job', VehicleItem for 'vehicle', RealEstateItem for 'real_estate', EducationItem for 'education', DestinationItem for 'destination', AppItem for 'app', or freeform objects for 'product', 'inventory', and 'promotion'. Mutually exclusive with url \u2014 provide one or the other, not both. Implementations should validate items against the type-specific schema.", + "items": { + "type": "object" + }, + "minItems": 1 + }, + "ids": { + "type": "array", + "description": "Filter catalog to specific item IDs. For offering-type catalogs, these are offering_id values. For product-type catalogs, these are SKU identifiers.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "gtins": { + "type": "array", + "description": "Filter product-type catalogs by GTIN identifiers for cross-retailer catalog matching. Accepts standard GTIN formats (GTIN-8, UPC-A/GTIN-12, EAN-13/GTIN-13, GTIN-14). Only applicable when type is 'product'.", + "items": { + "type": "string", + "pattern": "^[0-9]{8,14}$" + }, + "minItems": 1 + }, + "tags": { + "type": "array", + "description": "Filter catalog to items with these tags. Tags are matched using OR logic \u2014 items matching any tag are included.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "category": { + "type": "string", + "description": "Filter catalog to items in this category (e.g., 'beverages/soft-drinks', 'chef-positions')." + }, + "query": { + "type": "string", + "description": "Natural language filter for catalog items (e.g., 'all pasta sauces under $5', 'amsterdam vacancies')." + }, + "conversion_events": { + "type": "array", + "description": "Event types that represent conversions for items in this catalog. Declares what events the platform should attribute to catalog items \u2014 e.g., a job catalog converts via submit_application, a product catalog via purchase. The event's content_ids field carries the item IDs that connect back to catalog items. Use content_id_type to declare what identifier type content_ids values represent.", + "items": { + "$ref": "#/$defs/EventType" + }, + "minItems": 1, + "uniqueItems": true + }, + "content_id_type": { + "$ref": "#/$defs/ContentIDType" + }, + "feed_field_mappings": { + "type": "array", + "description": "Declarative normalization rules for external feeds. Maps non-standard feed field names, date formats, price encodings, and image URLs to the AdCP catalog item schema. Applied during sync_catalogs ingestion. Supports field renames, named transforms (date, divide, boolean, split), static literal injection, and assignment of image URLs to typed asset pools.", + "items": { + "title": "Catalog Field Mapping", + "description": "Declares how a field in an external feed maps to the AdCP catalog item schema. Used in sync_catalogs feed_field_mappings to normalize non-AdCP feeds (Google Merchant Center, LinkedIn Jobs XML, hotel XML, etc.) to the standard catalog item schema without requiring the buyer to preprocess every feed. Multiple mappings can assemble a nested object via dot notation (e.g., separate mappings for price.amount and price.currency).", + "type": "object", + "properties": { + "feed_field": { + "type": "string", + "description": "Field name in the external feed record. Omit when injecting a static literal value (use the value property instead)." + }, + "catalog_field": { + "type": "string", + "description": "Target field on the catalog item schema, using dot notation for nested fields (e.g., 'name', 'price.amount', 'location.city'). Mutually exclusive with asset_group_id." + }, + "asset_group_id": { + "type": "string", + "description": "Places the feed field value (a URL) into a typed asset pool on the catalog item's assets array. The value is wrapped as an image or video asset in a group with this ID. Use standard group IDs: 'images_landscape', 'images_vertical', 'images_square', 'logo', 'video'. Mutually exclusive with catalog_field." + }, + "value": { + "description": "Static literal value to inject into catalog_field for every item, regardless of what the feed contains. Mutually exclusive with feed_field. Useful for fields the feed omits (e.g., currency when price is always USD, or a constant category value)." + }, + "transform": { + "type": "string", + "description": "Named transform to apply to the feed field value before writing to the catalog schema. See transform-specific parameters (format, timezone, by, separator).", + "enum": [ + "date", + "divide", + "boolean", + "split" + ] + }, + "format": { + "type": "string", + "description": "For transform 'date': the input date format string (e.g., 'YYYYMMDD', 'MM/DD/YYYY', 'DD-MM-YYYY'). Output is always ISO 8601 (e.g., '2025-03-01'). Uses Unicode date pattern tokens." + }, + "timezone": { + "type": "string", + "description": "For transform 'date': the timezone of the input value. IANA timezone identifier (e.g., 'UTC', 'America/New_York', 'Europe/Amsterdam'). Defaults to UTC when omitted." + }, + "by": { + "type": "number", + "description": "For transform 'divide': the divisor to apply (e.g., 100 to convert integer cents to decimal dollars).", + "exclusiveMinimum": 0 + }, + "separator": { + "type": "string", + "description": "For transform 'split': the separator character or string to split on. Defaults to ','.", + "default": "," + }, + "default": { + "description": "Fallback value to use when feed_field is absent, null, or empty. Applied after any transform would have been applied. Allows optional feed fields to have a guaranteed baseline value." + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "allOf": [ + { + "not": { + "required": [ + "feed_field", + "value" + ] + } + }, + { + "not": { + "required": [ + "catalog_field", + "asset_group_id" + ] + } + } + ], + "additionalProperties": true, + "examples": [ + { + "description": "Simple field rename", + "data": { + "feed_field": "hotel_name", + "catalog_field": "name" + } + }, + { + "description": "Date format transform \u2014 YYYYMMDD to ISO 8601", + "data": { + "feed_field": "available_from", + "catalog_field": "valid_from", + "transform": "date", + "format": "YYYYMMDD", + "timezone": "UTC" + } + }, + { + "description": "Divide cents to dollars", + "data": { + "feed_field": "price_cents", + "catalog_field": "price.amount", + "transform": "divide", + "by": 100 + } + }, + { + "description": "Static literal injection \u2014 currency not in feed", + "data": { + "catalog_field": "price.currency", + "value": "USD" + } + }, + { + "description": "Image URL assigned to typed asset pool", + "data": { + "feed_field": "primary_photo_url", + "asset_group_id": "images_landscape" + } + }, + { + "description": "Vertical photo URL assigned to vertical pool", + "data": { + "feed_field": "portrait_photo_url", + "asset_group_id": "images_vertical" + } + }, + { + "description": "Split comma-separated tags to array", + "data": { + "feed_field": "amenity_list", + "catalog_field": "amenities", + "transform": "split", + "separator": "," + } + }, + { + "description": "Boolean coercion with default", + "data": { + "feed_field": "is_available", + "catalog_field": "available", + "transform": "boolean", + "default": false + } + } + ] + }, + "minItems": 1 + } + }, + "required": [ + "type" + ], + "additionalProperties": true, + "examples": [ + { + "description": "Synced product catalog from Google Merchant Center", + "data": { + "catalog_id": "gmc-primary", + "name": "Primary Product Feed", + "type": "product", + "url": "https://feeds.acmecorp.com/products.xml", + "feed_format": "google_merchant_center", + "update_frequency": "daily" + } + }, + { + "description": "Inventory feed for store-level stock data", + "data": { + "catalog_id": "store-inventory", + "name": "Store Inventory", + "type": "inventory", + "url": "https://feeds.acmecorp.com/inventory.json", + "feed_format": "custom", + "update_frequency": "hourly" + } + }, + { + "description": "Store locator feed", + "data": { + "catalog_id": "retail-locations", + "name": "Retail Locations", + "type": "store", + "url": "https://feeds.acmecorp.com/stores.json", + "feed_format": "custom", + "update_frequency": "weekly" + } + }, + { + "description": "Promotional pricing feed", + "data": { + "catalog_id": "summer-sale", + "name": "Summer Sale Promotions", + "type": "promotion", + "url": "https://feeds.acmecorp.com/promotions.json", + "feed_format": "google_merchant_center", + "update_frequency": "daily" + } + }, + { + "description": "Inline offering catalog (no sync needed)", + "data": { + "type": "offering", + "items": [ + { + "offering_id": "summer-sale", + "name": "Summer Sale", + "landing_url": "https://acme.com/summer" + } + ] + } + }, + { + "description": "Reference to a previously synced catalog by ID", + "data": { + "catalog_id": "gmc-primary", + "type": "product", + "ids": [ + "SKU-12345", + "SKU-67890" + ] + } + }, + { + "description": "Product catalog with GTIN cross-retailer matching and attribution", + "data": { + "type": "product", + "gtins": [ + "00013000006040", + "00013000006057" + ], + "content_id_type": "gtin", + "conversion_events": [ + "purchase", + "add_to_cart" + ] + } + }, + { + "description": "Inline store catalog with catchment areas", + "data": { + "catalog_id": "retail-locations", + "name": "Retail Locations", + "type": "store", + "items": [ + { + "store_id": "amsterdam-flagship", + "name": "Amsterdam Flagship", + "location": { + "lat": 52.3676, + "lng": 4.9041 + }, + "catchments": [ + { + "catchment_id": "walk", + "travel_time": { + "value": 10, + "unit": "min" + }, + "transport_mode": "walking" + }, + { + "catchment_id": "drive", + "travel_time": { + "value": 15, + "unit": "min" + }, + "transport_mode": "driving" + } + ] + } + ] + } + } + ] + } + }, + "format_ids": { + "type": "array", + "description": "Legacy named-format IDs active for this package. Echoed from the create_media_buy request; omitted means all formats for the product are active unless `format_option_refs` narrows the 3.1+ format-option set.", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + } + }, + "format_option_refs": { + "type": "array", + "description": "Structured 3.1+ format option references active for this package, echoed from the create_media_buy request. Publisher-catalog-backed options are identified by `{ scope: \"publisher\", publisher_domain, format_option_id }`; product-local options are identified by `{ scope: \"product\", format_option_id }` and resolve only against this package's target product. Omitted means all 3.1+ format options for the product are active unless `format_ids` narrows the set.", + "items": { + "title": "Format Option Reference", + "description": "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.", + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "title": "Publisher Catalog Format Option Reference", + "description": "Selects a publisher-catalog-backed product format option by publisher domain and format option ID.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "publisher", + "description": "Reference resolves against the named publisher's adagents.json top-level `formats[]` catalog." + }, + "publisher_domain": { + "type": "string", + "description": "Publisher domain where the adagents.json declaring this format option is hosted.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "format_option_id": { + "type": "string", + "description": "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[]`." + } + }, + "required": [ + "scope", + "publisher_domain", + "format_option_id" + ], + "additionalProperties": true + }, + { + "title": "Product-Local Format Option Reference", + "description": "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.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Reference resolves only against the target product's inline `format_options[]`." + }, + "format_option_id": { + "type": "string", + "description": "Stable format option ID from the target product's inline `format_options[]`." + }, + "publisher_domain": false + }, + "required": [ + "scope", + "format_option_id" + ], + "additionalProperties": true + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "targeting_overlay": { + "title": "Targeting Overlay", + "description": "Optional restriction overlays for media buys. Most targeting should be expressed in the brief and handled by the publisher. These fields are for functional restrictions: geographic (RCT testing, regulatory compliance, proximity targeting), age verification (alcohol, gambling), device platform (app compatibility), language (localization), and keyword targeting (search/retail media).", + "type": "object", + "properties": { + "geo_countries": { + "type": "array", + "description": "Restrict delivery to specific countries. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "geo_countries_exclude": { + "type": "array", + "description": "Exclude specific countries from delivery. ISO 3166-1 alpha-2 codes (e.g., 'US', 'GB', 'DE').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "minItems": 1 + }, + "geo_regions": { + "type": "array", + "description": "Restrict delivery to specific regions/states. ISO 3166-2 subdivision codes (e.g., 'US-CA', 'GB-SCT').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$" + }, + "minItems": 1 + }, + "geo_regions_exclude": { + "type": "array", + "description": "Exclude specific regions/states from delivery. ISO 3166-2 subdivision codes (e.g., 'US-CA', 'GB-SCT').", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$" + }, + "minItems": 1 + }, + "geo_metros": { + "type": "array", + "description": "Restrict delivery to specific metro areas. Each entry specifies the classification system and target values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/MetroAreaSystem" + }, + "values": { + "type": "array", + "description": "Metro codes within the system (e.g., ['501', '602'] for Nielsen DMAs)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "geo_metros_exclude": { + "type": "array", + "description": "Exclude specific metro areas from delivery. Each entry specifies the classification system and excluded values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/MetroAreaSystem" + }, + "values": { + "type": "array", + "description": "Metro codes to exclude within the system (e.g., ['501', '602'] for Nielsen DMAs)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "geo_postal_areas": { + "type": "array", + "description": "Restrict delivery to specific postal areas. Each entry specifies the postal system and target values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/PostalCodeSystem" + }, + "values": { + "type": "array", + "description": "Postal codes within the system (e.g., ['10001', '10002'] for us_zip)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "geo_postal_areas_exclude": { + "type": "array", + "description": "Exclude specific postal areas from delivery. Each entry specifies the postal system and excluded values. Seller must declare supported systems in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "system": { + "$ref": "#/$defs/PostalCodeSystem" + }, + "values": { + "type": "array", + "description": "Postal codes to exclude within the system (e.g., ['10001', '10002'] for us_zip)", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "system", + "values" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "daypart_targets": { + "type": "array", + "description": "Restrict delivery to specific time windows. Each entry specifies days of week and an hour range.", + "items": { + "title": "Daypart Target", + "description": "A time window for daypart targeting. Specifies days of week and an hour range. start_hour is inclusive, end_hour is exclusive (e.g., 6-10 = 6:00am to 10:00am). Follows the Google Ads AdScheduleInfo / DV360 DayPartTargeting pattern.", + "type": "object", + "properties": { + "days": { + "type": "array", + "description": "Days of week this window applies to. Use multiple days for compact targeting (e.g., monday-friday in one object).", + "items": { + "$ref": "#/$defs/DayOfWeek" + }, + "minItems": 1 + }, + "start_hour": { + "type": "integer", + "description": "Start hour (inclusive), 0-23 in 24-hour format. 0 = midnight, 6 = 6:00am, 18 = 6:00pm.", + "minimum": 0, + "maximum": 23 + }, + "end_hour": { + "type": "integer", + "description": "End hour (exclusive), 1-24 in 24-hour format. 10 = 10:00am, 24 = midnight. Must be greater than start_hour.", + "minimum": 1, + "maximum": 24 + }, + "label": { + "type": "string", + "description": "Optional human-readable name for this time window (e.g., 'Morning Drive', 'Prime Time')" + } + }, + "required": [ + "days", + "start_hour", + "end_hour" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "axe_include_segment": { + "type": "string", + "description": "Deprecated: Use TMP provider fields instead. AXE segment ID to include for targeting.", + "deprecated": true + }, + "axe_exclude_segment": { + "type": "string", + "description": "Deprecated: Use TMP provider fields instead. AXE segment ID to exclude from targeting.", + "deprecated": true + }, + "audience_include": { + "type": "array", + "description": "Restrict delivery to members of these first-party CRM audiences. Only users present in the uploaded lists are eligible. References audience_id values from sync_audiences on the same seller account \u2014 audience IDs are not portable across sellers. Not for lookalike expansion \u2014 express that intent in the campaign brief. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "audience_exclude": { + "type": "array", + "description": "Suppress delivery to members of these first-party CRM audiences. Matched users are excluded regardless of other targeting. References audience_id values from sync_audiences on the same seller account \u2014 audience IDs are not portable across sellers. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "signal_targeting_groups": { + "title": "Package Signal Targeting Groups", + "description": "Basic Boolean grouping for seller-offered signals. v1 supports a required top-level operator 'all' and child groups with operator 'any' for include groups or 'none' for exclusion groups. Example semantics: group 1 any(A, B) plus group 2 none(C, D) means (A OR B) AND NOT (C OR D). Signal entries reference named signal definitions with signal_ref scope 'product' for product-local signal options or scope 'data_provider' for external published adagents.json signal catalogs. For simple include-only targeting, send one child group with operator 'any'. Sellers SHOULD reject entries that are not available for the product through inline signal_targeting_options or get_signals, are not active for the account, or exceed the product's signal_targeting_allowed/signal_targeting_rules/product terms. Signal targeting limits are product-scoped, not declared in get_adcp_capabilities, because products may be backed by different ad servers. Sellers MUST echo applied signal_targeting_groups on the resulting package state, including fixed/default selections. On update_media_buy, sellers MAY reject changes that require repricing with REQUOTE_REQUIRED.", + "type": "object", + "properties": { + "operator": { + "type": "string", + "enum": [ + "all" + ], + "description": "Groups-level operator. Required even though v1 only supports 'all': every child group must be satisfied." + }, + "groups": { + "type": "array", + "description": "Signal targeting groups to evaluate. Use operator 'any' for include groups and 'none' for exclusion groups.", + "items": { + "title": "Package Signal Targeting Group", + "description": "A basic Boolean group of package-level signal targeting entries. 'any' means the user must match at least one signal in the group. 'none' means the user must match none of the signals in the group. Use groups for portable include/exclude composition such as (A OR B) AND NOT (C OR D).", + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "How to evaluate the signals in this group. 'any' is an OR include group. 'none' is an exclusion group equivalent to NOT (A OR B OR C).", + "enum": [ + "any", + "none" + ] + }, + "signals": { + "type": "array", + "description": "Signal targeting entries evaluated by this group. Each entry uses the package signal targeting shape, including signal_ref, value expression, and optional pricing, execution-handle, or activation fields.", + "items": { + "title": "Package Signal Targeting", + "description": "Buy-time selection of one seller-offered signal inside a package signal targeting group. The signal_ref uses scope 'product' for a product-local signal option, scope 'data_provider' for a signal defined by a data provider's published adagents.json signal catalog, or scope 'signal_source' for a source-native signal that is not catalog-published. The selected product's inline Product.signal_targeting_options, get_signals feed, and signal_targeting_rules define buy-time eligibility. Inclusion and exclusion are controlled by the parent group operator: use operator 'any' to include users matching the signal expression and operator 'none' to exclude users matching the signal expression. For binary signals, value MUST be true; do not use value=false for exclusion inside signal_targeting_groups. Use audience_include/audience_exclude only for buyer-managed first-party audiences registered through sync_audiences.", + "type": "object", + "allOf": [ + { + "title": "Signal Targeting Expression", + "description": "Predicate over a named signal definition. Signals are typed dimensions, similar to feature values: binary signals match true, categorical signals match one of a set of values, and numeric signals match a range. In package signal targeting groups, include/exclude semantics are controlled by the parent group operator, not by negating the expression.", + "discriminator": { + "propertyName": "value_type" + }, + "oneOf": [ + { + "type": "object", + "description": "Binary signal expression. In grouped package targeting, value is always true; use a parent group with operator 'none' for exclusion.", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Named signal being targeted.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "binary", + "description": "Discriminator for binary signals." + }, + "value": { + "type": "boolean", + "const": true, + "description": "Binary package signal entries match users for whom the signal is true. Use the parent group operator for include/exclude." + } + }, + "required": [ + "signal_ref", + "value_type", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Categorical signal expression - target users with one of the listed values.", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Named signal being targeted.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "categorical", + "description": "Discriminator for categorical signals." + }, + "values": { + "type": "array", + "description": "Values to target. Users with any of these values match the expression.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "signal_ref", + "value_type", + "values" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Numeric signal expression - target users within a value range. At least one of min_value or max_value is required. If both min_value and max_value are provided, min_value MUST be <= max_value.", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "Named signal being targeted.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "numeric", + "description": "Discriminator for numeric signals." + }, + "min_value": { + "type": "number", + "description": "Minimum value, inclusive. Omit for no minimum. Should be within the signal definition's range when declared." + }, + "max_value": { + "type": "number", + "description": "Maximum value, inclusive. Omit for no maximum. Should be within the signal definition's range when declared." + } + }, + "required": [ + "signal_ref", + "value_type" + ], + "anyOf": [ + { + "required": [ + "min_value" + ] + }, + { + "required": [ + "max_value" + ] + } + ], + "additionalProperties": true + } + ] + } + ], + "properties": { + "pricing_option_id": { + "type": "string", + "description": "Pricing option selected for this signal. Use the pricing_option_id from the product's signal_targeting_options entry when product-scoped pricing is present; otherwise use the seller get_signals pricing only when the product option does not override it. Required when the selected signal has pricing_options; omit only when the signal is bundled into the product price or has no incremental cost.", + "x-entity": "vendor_pricing_option" + }, + "signal_agent_segment_id": { + "type": "string", + "description": "Optional opaque seller execution handle for this signal. Omit when signal_ref is sufficient for the seller to resolve the signal. Include only when the product option exposes a separate runtime or activation handle that differs from the named signal reference.", + "x-entity": "signal_activation_id" + }, + "activation_key": { + "title": "Activation Key", + "description": "Destination-specific activation key returned by get_signals or activate_signal. Usually omitted for seller-offered signals selected directly through the same seller; include only when the selected signal was separately activated and the seller requires the activation key to correlate the package selection.", + "type": "object", + "discriminator": { + "propertyName": "type" + }, + "oneOf": [ + { + "properties": { + "type": { + "type": "string", + "const": "segment_id", + "description": "Segment ID based targeting" + }, + "segment_id": { + "type": "string", + "description": "The platform-specific segment identifier to use in campaign targeting" + } + }, + "required": [ + "type", + "segment_id" + ], + "additionalProperties": true + }, + { + "properties": { + "type": { + "type": "string", + "const": "key_value", + "description": "Key-value pair based targeting" + }, + "key": { + "type": "string", + "description": "The targeting parameter key" + }, + "value": { + "type": "string", + "description": "The targeting parameter value" + } + }, + "required": [ + "type", + "key", + "value" + ], + "additionalProperties": true + } + ] + } + }, + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "operator", + "signals" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "operator", + "groups" + ], + "additionalProperties": true + }, + "frequency_cap": { + "title": "Frequency Cap", + "description": "Frequency capping settings for package-level application. Two types of frequency control can be used independently or together: suppress enforces a cooldown between consecutive exposures; max_impressions + per + window caps total exposures per entity in a time window. When both suppress and max_impressions are set, an impression is delivered only if both constraints permit it (AND semantics). At least one of suppress, suppress_minutes, or max_impressions must be set.", + "type": "object", + "properties": { + "suppress": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Cooldown period between consecutive exposures to the same entity. Prevents back-to-back ad delivery (e.g. {\"interval\": 60, \"unit\": \"minutes\"} for a 1-hour cooldown). Preferred over suppress_minutes." + }, + "suppress_minutes": { + "type": "number", + "description": "Deprecated \u2014 use suppress instead. Cooldown period in minutes between consecutive exposures to the same entity (e.g. 60 for a 1-hour cooldown).", + "minimum": 0 + }, + "max_impressions": { + "type": "integer", + "description": "Maximum number of impressions per entity per window. For duration windows, implementations typically use a rolling window; 'campaign' applies a fixed cap across the full flight.", + "minimum": 1 + }, + "per": { + "allOf": [ + { + "$ref": "#/$defs/ReachUnit" + } + ], + "description": "Entity granularity for impression counting. Required when max_impressions is set." + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Time window for the max_impressions cap (e.g. {\"interval\": 7, \"unit\": \"days\"} or {\"interval\": 1, \"unit\": \"campaign\"} for the full flight). Required when max_impressions is set." + } + }, + "anyOf": [ + { + "required": [ + "suppress" + ] + }, + { + "required": [ + "suppress_minutes" + ] + }, + { + "required": [ + "max_impressions" + ] + } + ], + "dependencies": { + "max_impressions": [ + "per", + "window" + ], + "per": [ + "max_impressions" + ], + "window": [ + "max_impressions" + ] + }, + "additionalProperties": true + }, + "property_list": { + "title": "Property List Reference", + "description": "Reference to a property list for targeting specific properties within this product. The package runs on the intersection of the product's publisher_properties and this list. Sellers SHOULD return a validation error if the product has property_targeting_allowed: false.", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent managing the property list" + }, + "list_id": { + "type": "string", + "description": "Identifier for the property list within the agent", + "minLength": 1, + "x-entity": "property_list" + }, + "auth_token": { + "type": "string", + "description": "JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access." + } + }, + "required": [ + "agent_url", + "list_id" + ], + "additionalProperties": false + }, + "collection_list": { + "title": "Collection List Reference", + "description": "Reference to a collection list for including specific collections (programs, shows) within this product. The package runs on the intersection of matched collections and this list. Use for inclusion-based collection targeting. Seller must declare support in get_adcp_capabilities.", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent managing the collection list" + }, + "list_id": { + "type": "string", + "description": "Identifier for the collection list within the agent", + "minLength": 1, + "x-entity": "collection_list" + }, + "auth_token": { + "type": "string", + "description": "JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access." + } + }, + "required": [ + "agent_url", + "list_id" + ], + "additionalProperties": false + }, + "collection_list_exclude": { + "title": "Collection List Reference", + "description": "Reference to a collection list for excluding specific collections (programs, shows) from this product. Matched collections must not carry the buyer's ads. Use for brand safety do-not-air lists. Seller must declare support in get_adcp_capabilities.", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent managing the collection list" + }, + "list_id": { + "type": "string", + "description": "Identifier for the collection list within the agent", + "minLength": 1, + "x-entity": "collection_list" + }, + "auth_token": { + "type": "string", + "description": "JWT or other authorization token for accessing the list. Optional if the list is public or caller has implicit access." + } + }, + "required": [ + "agent_url", + "list_id" + ], + "additionalProperties": false + }, + "age_restriction": { + "type": "object", + "description": "Age restriction for compliance. Use for legal requirements (alcohol, gambling), not audience targeting.", + "properties": { + "min": { + "type": "integer", + "minimum": 13, + "maximum": 99, + "description": "Minimum age required" + }, + "verification_required": { + "type": "boolean", + "default": false, + "description": "Whether verified age (not inferred) is required for compliance" + }, + "accepted_methods": { + "type": "array", + "description": "Accepted verification methods. If omitted, any method the platform supports is acceptable.", + "items": { + "$ref": "#/$defs/AgeVerificationMethod" + }, + "minItems": 1 + } + }, + "required": [ + "min" + ], + "additionalProperties": false + }, + "device_platform": { + "type": "array", + "description": "Restrict to specific platforms. Use for technical compatibility (app only works on iOS). Values from Sec-CH-UA-Platform standard, extended for CTV.", + "items": { + "$ref": "#/$defs/DevicePlatform" + }, + "minItems": 1 + }, + "device_type": { + "type": "array", + "description": "Restrict to specific device form factors. Use for campaigns targeting hardware categories rather than operating systems (e.g., mobile-only promotions, CTV campaigns).", + "items": { + "$ref": "#/$defs/DeviceType" + }, + "minItems": 1 + }, + "device_type_exclude": { + "type": "array", + "description": "Exclude specific device form factors from delivery (e.g., exclude CTV for app-install campaigns).", + "items": { + "$ref": "#/$defs/DeviceType" + }, + "minItems": 1 + }, + "store_catchments": { + "type": "array", + "description": "Target users within store catchment areas from a synced store catalog. Each entry references a store-type catalog and optionally narrows to specific stores or catchment zones.", + "items": { + "type": "object", + "properties": { + "catalog_id": { + "type": "string", + "description": "Synced store-type catalog ID from sync_catalogs." + }, + "store_ids": { + "type": "array", + "description": "Filter to specific stores within the catalog. Omit to target all stores.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "catchment_ids": { + "type": "array", + "description": "Catchment zone IDs to target (e.g., 'walk', 'drive'). Omit to target all catchment zones.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "catalog_id" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "geo_proximity": { + "type": "array", + "description": "Target users within travel time, distance, or a custom boundary around arbitrary geographic points. Multiple entries use OR semantics \u2014 a user within range of any listed point is eligible. For campaigns targeting 10+ locations, consider using store_catchments with a location catalog instead. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "lat": { + "type": "number", + "minimum": -90, + "maximum": 90, + "description": "Latitude in decimal degrees (WGS 84). Required for travel_time and radius methods." + }, + "lng": { + "type": "number", + "minimum": -180, + "maximum": 180, + "description": "Longitude in decimal degrees (WGS 84). Required for travel_time and radius methods." + }, + "label": { + "type": "string", + "description": "Human-readable label for this entry (e.g., 'D\u00fcsseldorf', 'Heathrow Airport', 'Primary trade area')." + }, + "travel_time": { + "type": "object", + "description": "Travel time limit for isochrone calculation. The platform resolves this to a geographic boundary based on actual transportation networks.", + "properties": { + "value": { + "type": "number", + "minimum": 1, + "description": "Travel time limit." + }, + "unit": { + "$ref": "#/$defs/TravelTimeUnit" + } + }, + "required": [ + "value", + "unit" + ], + "additionalProperties": false + }, + "transport_mode": { + "$ref": "#/$defs/TransportMode" + }, + "radius": { + "type": "object", + "description": "Simple radius from the point. The platform draws a circle of this distance around the coordinates.", + "properties": { + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Radius distance." + }, + "unit": { + "$ref": "#/$defs/DistanceUnit" + } + }, + "required": [ + "value", + "unit" + ], + "additionalProperties": false + }, + "geometry": { + "type": "object", + "description": "Pre-computed GeoJSON geometry defining the proximity boundary. Use when the buyer has already calculated isochrones (via TravelTime, Mapbox, etc.) or has custom boundaries. When geometry is provided, lat/lng are not required.", + "properties": { + "type": { + "type": "string", + "enum": [ + "Polygon", + "MultiPolygon" + ], + "description": "GeoJSON geometry type." + }, + "coordinates": { + "type": "array", + "description": "GeoJSON coordinates array. For Polygon: array of linear rings. For MultiPolygon: array of polygons." + } + }, + "required": [ + "type", + "coordinates" + ], + "additionalProperties": false + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "oneOf": [ + { + "required": [ + "lat", + "lng", + "travel_time", + "transport_mode" + ], + "not": { + "anyOf": [ + { + "required": [ + "radius" + ] + }, + { + "required": [ + "geometry" + ] + } + ] + } + }, + { + "required": [ + "lat", + "lng", + "radius" + ], + "not": { + "anyOf": [ + { + "required": [ + "travel_time" + ] + }, + { + "required": [ + "geometry" + ] + } + ] + } + }, + { + "required": [ + "geometry" + ], + "not": { + "anyOf": [ + { + "required": [ + "travel_time" + ] + }, + { + "required": [ + "radius" + ] + } + ] + } + } + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "language": { + "type": "array", + "description": "Restrict to users with specific language preferences. ISO 639-1 codes (e.g., 'en', 'es', 'fr').", + "items": { + "type": "string", + "pattern": "^[a-z]{2}$" + }, + "minItems": 1 + }, + "keyword_targets": { + "type": "array", + "description": "Keyword targeting for search and retail media platforms. Restricts delivery to queries matching the specified keywords. Each keyword is identified by the tuple (keyword, match_type) \u2014 the same keyword string with different match types are distinct targets. Sellers SHOULD reject duplicate (keyword, match_type) pairs within a single request. Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "keyword": { + "type": "string", + "minLength": 1, + "description": "The keyword to target" + }, + "match_type": { + "$ref": "#/$defs/MatchType" + }, + "bid_price": { + "type": "number", + "minimum": 0, + "description": "Per-keyword bid price, denominated in the same currency as the package's pricing option. Overrides the package-level bid_price for this keyword. Inherits the max_bid interpretation from the pricing option: when max_bid is true, this is the keyword's bid ceiling; when false, this is the exact bid. If omitted, the package bid_price applies." + } + }, + "required": [ + "keyword", + "match_type" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "negative_keywords": { + "type": "array", + "description": "Keywords to exclude from delivery. Queries matching these keywords will not trigger the ad. Each negative keyword is identified by the tuple (keyword, match_type). Seller must declare support in get_adcp_capabilities.", + "items": { + "type": "object", + "properties": { + "keyword": { + "type": "string", + "minLength": 1, + "description": "The keyword to exclude" + }, + "match_type": { + "$ref": "#/$defs/MatchType" + } + }, + "required": [ + "keyword", + "match_type" + ], + "additionalProperties": false + }, + "minItems": 1 + }, + "signal_targeting": { + "type": "array", + "description": "DEPRECATED. Use signal_targeting_groups for package-level signal targeting. Legacy flat signal_targeting remains accepted during the SignalRef migration window but cannot express grouped include/exclude composition or product-scoped pricing.", + "deprecated": true, + "items": { + "title": "Signal Targeting", + "description": "Targeting constraint for a specific signal. Uses value_type as discriminator to determine the targeting expression format.", + "discriminator": { + "propertyName": "value_type" + }, + "oneOf": [ + { + "type": "object", + "description": "Binary signal targeting - user either matches or doesn't", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New targeting constraints SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "binary", + "description": "Discriminator for binary signals" + }, + "value": { + "type": "boolean", + "description": "Whether to include (true) or exclude (false) users matching this signal" + } + }, + "required": [ + "value_type", + "value" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Categorical signal targeting - target users with specific values", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New targeting constraints SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "categorical", + "description": "Discriminator for categorical signals" + }, + "values": { + "type": "array", + "description": "Values to target. Users with any of these values will be included.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "value_type", + "values" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Numeric signal targeting - target users within a value range. If min_value is provided, it must be <= max_value. Values should be within the signal's defined range (see signal definition).", + "properties": { + "signal_ref": { + "title": "Signal Ref", + "description": "The signal to target. New targeting constraints SHOULD use signal_ref.", + "x-entity": "signal", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "type": "object", + "description": "Product-scoped signal. The signal_id is meaningful only within the selected product/package context and MUST match a Product.included_signals[].signal_ref.signal_id or Product.signal_targeting_options[].signal_ref.signal_id for that product, depending on whether the signal is descriptive or selectable.", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Discriminator indicating the signal resolves through the selected product's included_signals or signal_targeting_options." + }, + "signal_id": { + "type": "string", + "description": "Product-local signal identifier. For local signals exposed on both get_signals and get_products, this MUST match get_signals.signals[].signal_ref.signal_id for the same signal.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Data-provider-scoped signal. The signal_id resolves through the data provider's published adagents.json signal catalog and can be authorization-verified against that catalog.", + "properties": { + "scope": { + "type": "string", + "const": "data_provider", + "description": "Discriminator indicating the signal resolves through a data provider's published adagents.json signal catalog." + }, + "data_provider_domain": { + "type": "string", + "description": "Domain that publishes the signal definition in its adagents.json signal catalog.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the data provider's published signal catalog.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "data_provider_domain", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "signal_source_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + }, + { + "type": "object", + "description": "Signal-source-scoped signal. Use this for source-native signals that are not published in an upstream adagents.json signal catalog. The buyer trusts the issuing signal source for this identity; use scope 'data_provider' instead when the signal is catalog-published, even if the catalog publisher is also the seller or signal source.", + "properties": { + "scope": { + "type": "string", + "const": "signal_source", + "description": "Discriminator indicating the signal resolves through the issuing signal source." + }, + "signal_source_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that issues this source-native signal." + }, + "signal_id": { + "type": "string", + "description": "Signal identifier within the issuing signal source's signal set.", + "pattern": "^[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "scope", + "signal_source_url", + "signal_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "data_provider_domain" + ] + }, + { + "required": [ + "agent_url" + ] + }, + { + "required": [ + "source" + ] + }, + { + "required": [ + "id" + ] + } + ] + }, + "additionalProperties": true + } + ] + }, + "signal_id": { + "title": "Signal ID", + "description": "DEPRECATED. Use signal_ref instead. Legacy SignalId retained for compatibility with older clients.", + "deprecated": true, + "x-entity": "signal", + "discriminator": { + "propertyName": "source" + }, + "oneOf": [ + { + "type": "object", + "description": "Catalog signal - references a signal from a data provider's published catalog. Buyers can verify authorization by checking the data provider's adagents.json.", + "properties": { + "source": { + "type": "string", + "const": "catalog", + "description": "Discriminator indicating this signal is from a data provider's published catalog" + }, + "data_provider_domain": { + "type": "string", + "description": "Domain of the data provider that owns this signal (e.g., 'pinnacle-data.example'). The signal definition is published at this domain's /.well-known/adagents.json", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the data provider's catalog (e.g., 'likely_ev_buyers', 'income_100k_plus')" + } + }, + "required": [ + "source", + "data_provider_domain", + "id" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Agent signal - references a signal native to a signal source identified by agent_url. Not externally verifiable through an upstream catalog; buyer trusts the issuing signal source's claim about the signal.", + "properties": { + "source": { + "type": "string", + "const": "agent", + "description": "Discriminator indicating this signal is native to the signal source identified by agent_url, not from a data provider catalog." + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the signal source that provides this signal (e.g., 'https://signals.example/.well-known/adcp/signals')" + }, + "id": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "Signal identifier within the agent's signal set (e.g., 'custom_auto_intenders')" + } + }, + "required": [ + "source", + "agent_url", + "id" + ], + "additionalProperties": true + } + ] + }, + "value_type": { + "type": "string", + "const": "numeric", + "description": "Discriminator for numeric signals" + }, + "min_value": { + "type": "number", + "description": "Minimum value (inclusive). Omit for no minimum. Must be <= max_value when both are provided. Should be >= signal's range.min if defined." + }, + "max_value": { + "type": "number", + "description": "Maximum value (inclusive). Omit for no maximum. Must be >= min_value when both are provided. Should be <= signal's range.max if defined." + } + }, + "required": [ + "value_type" + ], + "anyOf": [ + { + "required": [ + "signal_ref" + ] + }, + { + "required": [ + "signal_id" + ] + } + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + } + }, + "additionalProperties": true + }, + "measurement_terms": { + "title": "Measurement Terms", + "description": "Agreed billing measurement and makegood terms for this package. Reflects what was negotiated \u2014 may differ from the buyer's proposal or the product's defaults. When present, these terms are binding for the package's duration.", + "type": "object", + "properties": { + "billing_measurement": { + "type": "object", + "description": "Which vendor's count of the billing metric governs invoicing. The billing metric is determined by the pricing_model on the selected pricing_option (e.g., impressions for CPM, completed views for CPCV).", + "properties": { + "vendor": { + "title": "Brand Reference", + "description": "Vendor whose measurement of the billing metric is authoritative for invoicing (e.g., { domain: 'campaignmanager.google.com' } for buyer's DCM, { domain: 'admanager.google.com' } for seller's GAM).", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "max_variance_percent": { + "type": "number", + "minimum": 0, + "exclusiveMaximum": 100, + "description": "Maximum acceptable variance between the billing vendor's count and the other party's count before resolution is triggered (e.g., 10 means a 10% divergence triggers review)." + }, + "measurement_window": { + "type": "string", + "description": "Which measurement maturation stage the billing metric is reconciled against. References a window_id from the product's reporting_capabilities.measurement_windows. Examples: 'c7' for broadcast TV guarantees (live + 7 days DVR), 'final' for DOOH after IVT/fraud-check processing, 'post_sivt' for digital after sophisticated invalid-traffic filtering, 'downloads_30d' for podcast. When absent, billing is based on the seller's standard reporting without windowed maturation.", + "examples": [ + "live", + "c3", + "c7", + "tentative", + "final", + "post_ivt", + "post_sivt", + "downloads_30d" + ] + }, + "finalization_deadline_hours": { + "type": "integer", + "minimum": 0, + "description": "Maximum hours by which the authoritative party MUST publish a final record (`is_final: true` / `finalized_at` on `get_media_buy_delivery`, or `final: true` / `finalized_at` on `report_usage`). **Anchor:** when `measurement_window` is set, hours are counted from the close of that window (e.g., 240h after `c7` close = ~10 days after the 7-day DVR accumulation completes); when `measurement_window` is absent, hours are counted from `reporting_period.end`. Picking a single anchor avoids ambiguity for windowed channels where `reporting_period.end` and window close differ by days. The deadline applies to whichever party is named in `vendor` \u2014 seller, buyer, or third-party vendor \u2014 symmetrically. When the deadline elapses without a final record, the counterparty MAY fall back to its own attestation for invoicing (seller falls back to seller-attested numbers via `get_media_buy_delivery`; buyer falls back to a buyer-attested `report_usage` push), and the breach is treated like any other measurement-terms breach under `makegood_policy`. Absent means no contractual deadline \u2014 finalization is best-effort and disagreements resolve out of band." + } + }, + "required": [ + "vendor" + ], + "additionalProperties": true + }, + "makegood_policy": { + "type": "object", + "description": "Remedies available when a performance standard or billing measurement variance is breached. Seller declares which remedy types they support. When a breach occurs, the seller proposes a remedy from this menu; the buyer accepts or disputes.", + "properties": { + "available_remedies": { + "type": "array", + "description": "Remedy types the seller supports. Ordered by seller preference (first = preferred). Seller proposes from this list when a breach occurs; buyer accepts or disputes.", + "items": { + "$ref": "#/$defs/MakegoodRemedy" + }, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "available_remedies" + ], + "additionalProperties": true + } + }, + "additionalProperties": true + }, + "performance_standards": { + "type": "array", + "description": "Agreed performance standards for this package. When any entry specifies a vendor, creatives assigned to this package MUST include corresponding tracker_script or tracker_pixel assets from that vendor.", + "items": { + "title": "Performance Standard", + "description": "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).", + "type": "object", + "properties": { + "metric": { + "$ref": "#/$defs/PerformanceStandardMetric" + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "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." + }, + "standard": { + "$ref": "#/$defs/ViewabilityStandard" + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor measuring this metric (e.g., { domain: 'doubleverify.com' }). The vendor's brand.json agents array (type: 'measurement') is the discovery point for their measurement agent. When specified on a confirmed package, creatives MUST include tracker_script or tracker_pixel assets from this vendor.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + } + }, + "required": [ + "metric", + "threshold", + "vendor" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "committed_metrics": { + "type": "array", + "description": "The binding reporting contract for this package \u2014 what the seller has agreed to populate in delivery reports. Each entry carries an explicit `committed_at` timestamp, so the array also serves as the contract amendment ledger: day-1 commitments share `committed_at = create_media_buy.confirmed_at`; mid-flight additions carry their own timestamps. The `missing_metrics` field on `get_media_buy_delivery` reconciles against this list, filtering to entries where `committed_at < reporting_period.end` (a metric committed mid-flight is only audited from its commitment timestamp forward). Sellers stamp the day-1 set on the `create_media_buy` response; mid-flight additions are appended via `update_media_buy` (append-only \u2014 sellers MUST reject attempts to modify or remove existing entries with `validation_error`, suggested code: `IMMUTABLE_FIELD`). Optional in v1; absence means the seller does not provide an audit-grade contract and `missing_metrics` falls back to the product's live `available_metrics` (a known audit gap \u2014 buyers SHOULD treat absence as 'no audit-grade contract' rather than 'clean delivery'). 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. The unified shape is symmetric with `missing_metrics` and `aggregated_totals.metric_aggregates` \u2014 same atomic unit `(scope, metric_id, qualifier)` across contract, diff, and delivery, so reconciliation collapses to a row-level join on the tuple. Replaces the parallel-array design that shipped briefly in #3510.", + "items": { + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "properties": { + "scope": { + "type": "string", + "const": "standard", + "description": "Standard metric from the closed `available-metric.json` enum." + }, + "metric_id": { + "$ref": "#/$defs/AvailableMetric" + }, + "qualifier": { + "type": "object", + "description": "Disambiguates metrics whose definition varies by qualifier. Today carries five keys \u2014 `viewability_standard` (MRC vs GroupM viewability), `completion_source` (seller- vs vendor-attested completion), `attribution_methodology` (how attribution was computed for outcome metrics), `attribution_window` (the time window over which outcomes were attributed), and `lift_dimension` (which dimension of brand_lift this row represents \u2014 awareness, consideration, etc.). Required when the underlying `metric_id` has multiple incompatible measurement paths AND the seller commits to a specific one. Symmetric on `missing_metrics`. Reserved for additive qualifiers in future minors \u2014 schema is closed (`additionalProperties: false`); new keys ship explicitly. **Heterogeneous value types**: qualifier values can be either string enums (`viewability_standard`, `completion_source`, `attribution_methodology`, `lift_dimension`) or structured objects (`attribution_window` is a duration `{interval, unit}`). Consumers MUST dispatch on key name to know value shape; structured-value qualifiers join on canonical (key-sorted) deep equality so `{interval: 14, unit: 'days'}` and `{unit: 'days', interval: 14}` resolve to the same partition. Rate-style metrics (`new_to_brand_rate`, `engagement_rate`, etc.) inherit the methodology of their numerator \u2014 when a rate carries `attribution_methodology` qualifier, it applies to the underlying conversions/events being rated.", + "properties": { + "viewability_standard": { + "$ref": "#/$defs/ViewabilityStandard" + }, + "completion_source": { + "$ref": "#/$defs/CompletionSource" + }, + "attribution_methodology": { + "$ref": "#/$defs/AttributionMethodology" + }, + "attribution_window": { + "title": "Duration", + "description": "Time window over which outcome attribution is computed. **Object-valued, not string** \u2014 MUST be a structured duration object like `{interval: 14, unit: 'days'}`, NEVER a shorthand string like `'14d'`. SHOULD be set when `metric_id` is an outcome metric and the seller commits to a specific window. Common windows: `{interval: 7, unit: 'days'}`, `{interval: 14, unit: 'days'}`, `{interval: 30, unit: 'days'}`, `{interval: 90, unit: 'days'}`. Two outcome rows with the same `metric_id` and `attribution_methodology` but different `attribution_window` represent the same metric measured over different periods \u2014 the join on `(metric_id, qualifier)` keeps them as separate rows so buyers don't accidentally aggregate across windows.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + }, + "lift_dimension": { + "$ref": "#/$defs/LiftDimension" + } + }, + "additionalProperties": false + }, + "committed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this metric became part of the contract. Day-1 commitments use `create_media_buy.confirmed_at`; mid-flight additions use the time the amendment was accepted." + } + }, + "required": [ + "scope", + "metric_id", + "committed_at" + ], + "additionalProperties": false + }, + { + "properties": { + "scope": { + "type": "string", + "const": "vendor", + "description": "Vendor-defined metric, identified by the tuple `(vendor, metric_id)`." + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. The vendor's `brand.json` `agents[type='measurement']` is the canonical anchor.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary.", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + }, + "committed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this vendor metric became part of the contract." + } + }, + "required": [ + "scope", + "vendor", + "metric_id", + "committed_at" + ], + "additionalProperties": false + } + ] + }, + "minItems": 1, + "examples": [ + [ + { + "scope": "standard", + "metric_id": "impressions", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "standard", + "metric_id": "spend", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "standard", + "metric_id": "completed_views", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "vendor", + "vendor": { + "domain": "attentionvendor.example" + }, + "metric_id": "attention_units", + "committed_at": "2026-04-29T10:53:00Z" + }, + { + "scope": "standard", + "metric_id": "viewable_rate", + "qualifier": { + "viewability_standard": "mrc" + }, + "committed_at": "2026-05-30T14:22:00Z" + } + ] + ] + }, + "creative_assignments": { + "type": "array", + "description": "Creative assets assigned to this package", + "items": { + "title": "Creative Assignment", + "description": "Assignment of a creative asset to a package with optional placement targeting. Used in create_media_buy and update_media_buy requests. Note: sync_creatives does not support placement_refs or placement_ids - use create/update_media_buy for placement-level targeting.", + "type": "object", + "properties": { + "creative_id": { + "type": "string", + "description": "Unique identifier for the creative", + "x-entity": "creative" + }, + "weight": { + "type": "number", + "description": "Relative delivery weight for this creative (0\u2013100). When multiple creatives are assigned to the same package, weights determine impression distribution proportionally \u2014 a creative with weight 2 gets twice the delivery of weight 1. When omitted, the creative receives equal rotation with other unweighted creatives. A weight of 0 means the creative is assigned but paused (receives no delivery).", + "minimum": 0, + "maximum": 100 + }, + "placement_refs": { + "type": "array", + "description": "Optional array of structured placement references where this creative should run. New senders SHOULD use this field for placement-level targeting because placement IDs are publisher-scoped. When omitted, the creative runs on all buyer-targetable placements in the package. References entries from the product's `placements[]` array by `{ publisher_domain, placement_id }`; if `publisher_domain` is omitted in the ref, receivers MAY interpret it relative to the seller agent's own publisher domain in legacy single-publisher contexts. If both `placement_refs` and legacy `placement_ids` are present, `placement_refs` wins and receivers MUST ignore `placement_ids`.", + "items": { + "title": "Placement Reference", + "description": "Reference to a placement by publisher domain and placement ID. Placement IDs are publisher-scoped, matching the placement catalog in that publisher's adagents.json. When `publisher_domain` is omitted on legacy inputs, receivers MAY interpret the placement ID relative to the seller agent's own publisher domain; new senders SHOULD include `publisher_domain`.", + "type": "object", + "properties": { + "publisher_domain": { + "type": "string", + "description": "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.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "placement_id": { + "type": "string", + "description": "Placement ID from the publisher's adagents.json placement catalog, or an inline seller-defined placement ID interpreted within the same publisher namespace." + } + }, + "required": [ + "placement_id" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "placement_ids": { + "type": "array", + "description": "Legacy shorthand array of placement IDs where this creative should run. New senders SHOULD use `placement_refs` because placement IDs are publisher-scoped and strings are ambiguous in multi-publisher products. When omitted, the creative runs on all buyer-targetable placements in the package. Receivers MAY interpret string IDs relative to the seller agent's own publisher domain in legacy single-publisher contexts. If `placement_refs` is also present, receivers MUST ignore this field.", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "creative_id" + ], + "additionalProperties": true + } + }, + "format_ids_to_provide": { + "type": "array", + "description": "Format IDs that creative assets will be provided for this package", + "items": { + "title": "Format Reference (Structured Object)", + "description": "A JSON object \u2014 never a plain string \u2014 that identifies a creative format by its declaring agent and local slug. Required properties: agent_url (URI of the agent that owns the format) and id (slug matching [a-zA-Z0-9_-]+). Example: {\"agent_url\": \"https://creative.adcontextprotocol.org\", \"id\": \"display_300x250\"}. Can reference: (1) a concrete format with fixed dimensions (id only), (2) a template format without parameters (id only), or (3) a template format with parameters (id + dimensions/duration). Template formats accept parameters in format_id while concrete formats have fixed dimensions in their definition. Parameterized format IDs create unique, specific format variants. Using a plain string here is a schema violation.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + } + }, + "optimization_goals": { + "type": "array", + "description": "Optimization targets for this package. The seller optimizes delivery toward these goals in priority order. Common pattern: event goals (purchase, install) as primary targets at priority 1; metric goals (clicks, views) as secondary proxy signals at priority 2+.", + "items": { + "title": "Optimization Goal", + "description": "A single optimization target for a package. Packages accept an array of optimization_goals. When multiple goals are present, priority determines which the seller focuses on \u2014 1 is highest priority (primary goal); higher numbers are secondary. Duplicate priority values result in undefined seller behavior.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Optimize for a seller-tracked delivery metric. No event source required \u2014 the seller tracks these natively.", + "properties": { + "kind": { + "type": "string", + "const": "metric" + }, + "metric": { + "type": "string", + "enum": [ + "clicks", + "views", + "completed_views", + "viewed_seconds", + "attention_seconds", + "attention_score", + "engagements", + "follows", + "saves", + "profile_visits", + "reach" + ], + "description": "Seller-native metric to optimize for. Delivery metrics: clicks (link clicks, swipe-throughs, CTA taps that navigate away), views (viewable impressions), completed_views (video/audio completions \u2014 see view_duration_seconds), reach (unique audience reach \u2014 see reach_unit and target_frequency). Duration/score metrics: viewed_seconds (time in view per impression \u2014 reported back via `delivery-metrics.viewability.viewed_seconds`, governed by the viewability `standard`). Audience action metrics: engagements (any direct interaction with the ad unit beyond viewing \u2014 social reactions/comments/shares, story/unit opens, interactive overlay taps, companion banner interactions on audio and CTV), follows (new followers, page likes, artist/podcast/channel subscribes), saves (saves, bookmarks, playlist adds, pins \u2014 signals of intent to return), profile_visits (visits to the brand's in-platform page \u2014 profile, artist page, channel, or storefront. Does not include external website clicks, which are covered by 'clicks'). **DEPRECATED values** (slated for removal at next major): `attention_seconds` and `attention_score` \u2014 these have no industry-graduated definition (DoubleVerify, IAS, Adelaide, TVision, Lumen each define them differently) and cannot be meaningfully optimized for without a vendor binding. Use `kind: 'vendor_metric'` with an explicit `vendor` and `metric_id` instead \u2014 that path binds the goal to a specific measurement vendor and reconciles to the same `(vendor, metric_id)` key in delivery's `vendor_metric_values[]`. Sellers MAY reject the deprecated values with `TERMS_REJECTED` and a suggestion to use the `vendor_metric` kind." + }, + "reach_unit": { + "allOf": [ + { + "$ref": "#/$defs/ReachUnit" + } + ], + "description": "Unit for reach measurement. Required when metric is 'reach'. Must be a value declared in the product's metric_optimization.supported_reach_units." + }, + "target_frequency": { + "type": "object", + "description": "Target frequency band for reach optimization. Only applicable when metric is 'reach'. Frames frequency as an optimization signal: the seller should treat impressions toward entities already within the [min, max] band as lower-value, and impressions toward unreached entities as higher-value. This shifts budget toward fresh reach rather than re-reaching known users. When omitted, the seller maximizes unique reach without a frequency constraint. A hard cap can still be layered via targeting_overlay.frequency_cap if a ceiling is needed.", + "properties": { + "min": { + "type": "integer", + "minimum": 1, + "description": "Minimum frequency for an entity to be considered meaningfully reached within the window. Impressions that would bring an entity below this threshold are treated as high-value (growing reach). When omitted, the seller uses their platform default (typically 1)." + }, + "max": { + "type": "integer", + "minimum": 1, + "description": "Frequency at which an entity is considered saturated within the window. Impressions toward entities at or above this threshold are treated as lower-value. When both min and max are present, max must be greater than or equal to min. When omitted, the seller determines the saturation point." + }, + "window": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Time window over which frequency is measured (e.g. {\"interval\": 7, \"unit\": \"days\"} or {\"interval\": 1, \"unit\": \"campaign\"} for the full flight). Weekly windows are typical for brand campaigns; daily windows suit high-cadence direct response." + } + }, + "required": [ + "window" + ], + "anyOf": [ + { + "required": [ + "min" + ] + }, + { + "required": [ + "max" + ] + } + ], + "additionalProperties": true + }, + "view_duration_seconds": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Minimum video view duration in seconds that qualifies as a completed_view for this goal. Only applicable when metric is 'completed_views'. When omitted, the seller uses their platform default (typically 2\u201315 seconds). Common values: 2 (Snap/LinkedIn default), 6 (TikTok), 15 (Snap 15-second views, Meta ThruPlay). Sellers declare which durations they support in metric_optimization.supported_view_durations. Sellers must reject goals with unsupported values \u2014 silent rounding would create measurement discrepancies." + }, + "target": { + "description": "Target for this metric. When omitted, the seller optimizes for maximum metric volume within budget.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Target cost per unit of the metric (e.g., cost per click, cost per completed view).", + "properties": { + "kind": { + "type": "string", + "const": "cost_per" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target cost per metric unit in the buy currency" + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Minimum per-impression rate for this metric. The metric defines the units: proportions for count metrics (e.g., 0.001 for 0.1% CTR, 0.70 for 70% viewability), seconds for duration metrics (e.g., 3.0 for 3s in view), or score for score metrics.", + "properties": { + "kind": { + "type": "string", + "const": "threshold_rate" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Minimum per-impression value. Units depend on the metric: proportion (clicks, views, completed_views), seconds (viewed_seconds, attention_seconds), or score (attention_score)." + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + } + ] + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Relative priority among all optimization goals on this package. 1 = highest priority (primary goal); higher numbers are lower priority (secondary signals). When omitted, sellers may use array position as priority." + } + }, + "required": [ + "kind", + "metric" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Optimize for advertiser-tracked conversion events. Requires event sources registered via sync_event_sources.", + "properties": { + "kind": { + "type": "string", + "const": "event" + }, + "event_sources": { + "type": "array", + "description": "Event source and type pairs that feed this goal. Each entry identifies a source and event type to include. When the seller supports multi_source_event_dedup (declared in get_adcp_capabilities), they deduplicate by event_id across all entries \u2014 the same business event from multiple sources counts once, using value_field and value_factor from the first matching entry. When multi_source_event_dedup is false or absent, buyers should use a single entry per goal; the seller will use only the first entry. All event sources must be configured via sync_event_sources.", + "items": { + "type": "object", + "properties": { + "event_source_id": { + "type": "string", + "minLength": 1, + "description": "Event source to include (must be configured on this account via sync_event_sources)" + }, + "event_type": { + "$ref": "#/$defs/EventType" + }, + "custom_event_name": { + "type": "string", + "description": "Required when event_type is 'custom'. Platform-specific name for the custom event." + }, + "value_field": { + "type": "string", + "description": "Which field in the event's custom_data carries the monetary value. The seller must use this field for value extraction and aggregation when computing ROAS and conversion value metrics. Required on at least one entry when target.kind is 'per_ad_spend' or 'maximize_value' \u2014 sellers must reject these target kinds when no event source entry includes value_field. When present without a value-oriented target, the seller may use it for delivery reporting (conversion_value, roas) but must not change the optimization objective. Common values: 'value', 'order_total', 'profit_margin'. This is not passed as a parameter to underlying platform APIs \u2014 the seller maps it to their platform's value ingestion mechanism." + }, + "value_factor": { + "type": "number", + "default": 1, + "description": "Multiplier the seller must apply to value_field before aggregation. Use -1 for refund events (negate the value), 0.01 for values in cents, -0.01 for refunds in cents. A value of 0 zeroes out this source's value contribution (the source still counts for event dedup). Defaults to 1. This is not passed as a parameter to underlying platform APIs \u2014 the seller applies it when computing aggregated value metrics." + } + }, + "required": [ + "event_source_id", + "event_type" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "target": { + "description": "Target cost or return for this event goal. When omitted, the seller optimizes for maximum conversion count within budget \u2014 regardless of whether value_field is present on event sources. The presence of value_field alone does not change the optimization objective; it only makes value available for reporting. An explicit target of maximize_value or per_ad_spend is required to steer toward value.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Target cost per conversion event (after deduplication across event_sources).", + "properties": { + "kind": { + "type": "string", + "const": "cost_per" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target cost per event in the buy currency" + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Target return per unit of ad spend, calculated as sum(value_field * value_factor) / spend across all deduplicated events. Requires value_field on at least one event_sources entry.", + "properties": { + "kind": { + "type": "string", + "const": "per_ad_spend" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target return ratio (e.g., 4.0 means $4 of value per $1 spent)" + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Maximize total conversion value within budget, without a specific return ratio target. Steers spend toward higher-value conversions rather than maximizing conversion count. Requires value_field on at least one event_sources entry.", + "properties": { + "kind": { + "type": "string", + "const": "maximize_value" + } + }, + "required": [ + "kind" + ], + "additionalProperties": true + } + ] + }, + "attribution_window": { + "allOf": [ + { + "title": "Attribution Window", + "description": "Describes the attribution methodology and lookback windows used for conversion measurement. Enables cross-platform comparison by making attribution methodology transparent. Used as a `$ref` from `optimization-goal.json` (buyer's optimization-time attribution choice), `get-media-buy-delivery-response.json` (seller-declared attribution methodology in delivery reports), and similar surfaces. All fields are optional individually but at least one of `post_click`, `post_view`, or `model` SHOULD be populated; absence of `model` means the seller's default attribution model applies (typically `last_touch` per industry convention) \u2014 sellers SHOULD populate `model` explicitly when committing to a specific methodology.", + "type": "object", + "properties": { + "post_click": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Post-click attribution window. Conversions occurring within this duration after a click are attributed to the ad." + }, + "post_view": { + "allOf": [ + { + "title": "Duration", + "description": "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 \u2014 the window spans the full campaign flight.", + "type": "object", + "properties": { + "interval": { + "type": "integer", + "minimum": 1, + "description": "Number of time units. Must be 1 when unit is 'campaign'." + }, + "unit": { + "type": "string", + "enum": [ + "seconds", + "minutes", + "hours", + "days", + "campaign" + ], + "description": "Time unit. 'seconds' for sub-minute precision. 'campaign' spans the full campaign flight." + } + }, + "required": [ + "interval", + "unit" + ], + "additionalProperties": false + } + ], + "description": "Post-view attribution window. Conversions occurring within this duration after an ad impression (without click) are attributed to the ad." + }, + "model": { + "$ref": "#/$defs/AttributionModel" + } + }, + "additionalProperties": true + } + ], + "description": "Attribution window for this optimization goal \u2014 references the canonical `attribution-window` shape (post_click, post_view, model). Values must match an option declared in the seller's `conversion_tracking.attribution_windows` capability. Sellers MUST reject windows not in their declared capabilities. When the entire field is omitted, the seller uses their default window." + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Relative priority among all optimization goals on this package. 1 = highest priority (primary goal); higher numbers are lower priority (secondary signals). When omitted, sellers may use array position as priority." + } + }, + "required": [ + "kind", + "event_sources" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Optimize for a vendor-attested measurement metric. Use when the metric has no graduated industry-standard definition and must be reconciled to a specific vendor \u2014 e.g., attention (DoubleVerify, IAS, Adelaide, TVision, Lumen), panel-based brand lift (Kantar, Upwave, Cint), emissions (Scope3, Good-Loop), retail-media partner metrics. The vendor + metric_id pair binds buyer\u2192seller\u2192vendor end-to-end: the seller's bidding stack steers toward this specific vendor's measurement, and delivery reports the value via `vendor_metric_values[]` with the same `(vendor, metric_id)` key. Three preconditions for goal acceptance: (1) Discovery \u2014 the `metric_id` SHOULD appear in the vendor's published `measurement.metrics[]` catalog (queried from the vendor's `brand.json` `agents[type='measurement']`). Sellers SHOULD verify against a cached snapshot of the vendor's capability response; staleness handling is implementation-defined. SHOULD this minor while measurement-vendor adoption of AdCP-conformant capability publication catches up; tightens to MUST at the next minor. (2) Capability \u2014 the `(vendor, metric_id)` pair MUST appear in the product's `vendor_metric_optimization.supported_metrics[]`, and the goal's `target.kind` MUST appear in that entry's `supported_targets`. (3) Reporting coherence \u2014 the package's `committed_metrics[]` MUST include a matching `{ scope: 'vendor', vendor, metric_id }` entry. Sellers MUST reject goals failing the capability or reporting-coherence preconditions. Optimization without committed reporting is unverifiable and is therefore disallowed at the wire level. Precondition checks are seller-runtime; the schema's `required` only validates structural presence of `kind`, `vendor`, and `metric_id`.", + "properties": { + "kind": { + "type": "string", + "const": "vendor_metric" + }, + "vendor": { + "title": "Brand Reference", + "description": "Vendor that defines and computes this metric. Same shape as `vendor_metric_values.vendor`, `reporting_capabilities.vendor_metrics[].vendor`, and `vendor_metric_optimization.supported_metrics[].vendor` \u2014 symmetric across discovery, capability, commitment, optimization, and reporting surfaces.", + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "Domain where /.well-known/brand.json is hosted, or the brand's operating domain", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "brand_id": { + "title": "Brand ID", + "description": "Brand identifier within the house portfolio. Optional for single-brand domains.", + "type": "string", + "pattern": "^[a-z0-9_]+$", + "x-entity": "advertiser_brand", + "examples": [ + "tide", + "cheerios", + "air_jordan", + "nike", + "pampers" + ] + }, + "industries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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." + }, + "data_subject_contestation": { + "type": "object", + "description": "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 \u2192 house \u2192 missing.", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "email": { + "type": "string", + "format": "email" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "anyOf": [ + { + "required": [ + "url" + ] + }, + { + "required": [ + "email" + ] + } + ], + "additionalProperties": false + }, + "brand_kit_override": { + "type": "object", + "description": "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 \u2014 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` \u2014 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 \u2014 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 \u2014 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.", + "properties": { + "logo": { + "title": "Image Asset", + "description": "Override logo asset.", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + "colors": { + "type": "object", + "description": "Override brand colors (hex strings).", + "properties": { + "primary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "secondary": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + } + }, + "additionalProperties": true + }, + "voice": { + "type": "string", + "description": "Override brand-voice description for surface-composed text/audio output." + }, + "tagline": { + "type": "string", + "description": "Override tagline." + } + }, + "additionalProperties": true + } + }, + "required": [ + "domain" + ], + "additionalProperties": false, + "examples": [ + { + "domain": "nova-brands.com", + "brand_id": "spark" + }, + { + "domain": "nova-brands.com", + "brand_id": "glow" + }, + { + "domain": "acme-corp.com" + } + ] + }, + "metric_id": { + "title": "Vendor Metric ID", + "description": "Identifier for the metric within the vendor's vocabulary (e.g., `attention_score`, `attention_seconds`, `gco2e_per_impression`, `awareness_lift`). MUST be present in the vendor's published `measurement.metrics[]` catalog and in the product's `vendor_metric_optimization.supported_metrics[]`.", + "type": "string", + "x-entity": "vendor_metric", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z][a-z0-9_]*$", + "examples": [ + "attention_units", + "gco2e_per_impression", + "demographic_reach", + "co_view_index", + "incremental_lift_percent" + ] + }, + "target": { + "description": "Target for this vendor metric. When omitted, the seller optimizes for maximum metric volume / score within budget. `cost_per` and `threshold_rate` semantics mirror the same target kinds on the `metric` kind \u2014 units are vendor-defined and depend on the vendor's `measurement.metrics[]` declaration for this `metric_id`.", + "discriminator": { + "propertyName": "kind" + }, + "oneOf": [ + { + "type": "object", + "description": "Target cost per unit of the vendor metric (e.g., cost per attention-second).", + "properties": { + "kind": { + "type": "string", + "const": "cost_per" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Target cost per metric unit in the buy currency. Units of the metric are vendor-defined." + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + }, + { + "type": "object", + "description": "Minimum per-impression value for this vendor metric (e.g., attention_score \u2265 70).", + "properties": { + "kind": { + "type": "string", + "const": "threshold_rate" + }, + "value": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Minimum per-impression value. Units of the metric are vendor-defined." + } + }, + "required": [ + "kind", + "value" + ], + "additionalProperties": true + } + ] + }, + "priority": { + "type": "integer", + "minimum": 1, + "description": "Relative priority among all optimization goals on this package. 1 = highest priority (primary goal); higher numbers are lower priority (secondary signals). When omitted, sellers may use array position as priority." + } + }, + "required": [ + "kind", + "vendor", + "metric_id" + ], + "additionalProperties": true + } + ] + }, + "minItems": 1 + }, + "start_time": { + "type": "string", + "format": "date-time", + "not": { + "const": "asap" + }, + "description": "Flight start date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's start_time. Sellers SHOULD always include the resolved value in responses, even when inherited." + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Flight end date/time for this package in ISO 8601 format. When omitted, the package inherits the media buy's end_time. Sellers SHOULD always include the resolved value in responses, even when inherited." + }, + "paused": { + "type": "boolean", + "description": "Whether this package is paused by the buyer. Paused packages do not deliver impressions. Defaults to false.", + "default": false + }, + "canceled": { + "type": "boolean", + "description": "Whether this package has been canceled. Canceled packages stop delivery and cannot be reactivated. Defaults to false.", + "default": false + }, + "cancellation": { + "type": "object", + "description": "Cancellation metadata. Present only when canceled is true.", + "required": [ + "canceled_at", + "canceled_by" + ], + "properties": { + "canceled_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when this package was canceled." + }, + "canceled_by": { + "$ref": "#/$defs/CanceledBy" + }, + "reason": { + "type": "string", + "description": "Reason the package was canceled.", + "maxLength": 500 + }, + "acknowledged_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the seller acknowledged the cancellation. Confirms inventory has been released and billing stopped. Absent until the seller processes the cancellation." + } + }, + "additionalProperties": false + }, + "agency_estimate_number": { + "type": "string", + "maxLength": 100, + "description": "Agency estimate or authorization number for this package. Echoed from the buyer's request. When present on the package, takes precedence over the media buy-level estimate number." + }, + "creative_deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp for creative upload or change deadline for this package. After this deadline, creative changes are rejected. When absent, the media buy's creative_deadline applies." + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "package_id" + ], + "additionalProperties": true + } + }, + "valid_actions": { + "type": "array", + "description": "Flat-vocabulary actions the buyer can perform after this update. Saves a round-trip to get_media_buys. Deprecated in favor of `available_actions[]`, which carries `mode`, optional SLA, and optional `terms_ref`. Sellers SHOULD populate both during the 3.x deprecation window; consumers MUST prefer `available_actions[]` when both are present. Removed in 4.0.", + "items": { + "$ref": "#/$defs/MediaBuyValidAction" + } + }, + "available_actions": { + "type": "array", + "description": "Structured per-buy resolution of actions available after this update. Authoritative \u2014 see `get-media-buys-response.json` for full semantics.", + "items": { + "title": "Media Buy Available Action", + "description": "An action currently available on a media buy, resolved against the buy's current status, negotiated terms, account tier, and any buy-level overrides. Authoritative per-buy capability \u2014 buyer SDKs MUST read this rather than re-deriving from the product's `allowed_actions[]`, because divergence from the product template is expected (negotiated terms and IO addenda live on the deal, not the product SKU). The containing `available_actions[]` array is uniquely keyed by `action`; sellers MUST NOT emit two entries with the same `action` value (this is a contract-level invariant \u2014 JSON Schema `uniqueItems` only catches structurally identical objects, so validators MUST enforce action-uniqueness separately). Predicate evaluators consuming dotted paths like `available_actions.extend_flight.sla.response_max` MUST index by `action` rather than by array position. The `mode` and `sla` values are advisory at the moment of emission; sellers MAY resolve to a different mode by the time the mutation arrives (state can change), in which case the request is rejected with `ACTION_NOT_ALLOWED` (`reason: mode_mismatch`).", + "type": "object", + "properties": { + "action": { + "$ref": "#/$defs/MediaBuyValidAction" + }, + "mode": { + "$ref": "#/$defs/MediaBuyActionMode" + }, + "sla": { + "title": "SLA Window", + "description": "Optional SLA commitment for this action on this buy. Absence means no commitment, not zero commitment.", + "type": "object", + "properties": { + "response_max": { + "type": "string", + "description": "Maximum time from when the buyer issues the action to when the seller acknowledges receipt (mode-appropriate: synchronous response for self_serve, queue ack for requires_approval, proposal task creation for requires_proposal). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT5M", + "PT4H", + "P1D" + ] + }, + "completion_max": { + "type": "string", + "description": "Maximum time from buyer issuing the action to the seller completing it (mutation applied, proposal finalized, approval resolved). ISO 8601 duration.", + "pattern": "^P(?!$)(\\d+Y)?(\\d+M)?(\\d+D)?(T(\\d+H)?(\\d+M)?(\\d+S)?)?$", + "examples": [ + "PT1H", + "PT24H", + "P2D" + ] + } + }, + "additionalProperties": false + }, + "terms_ref": { + "type": "string", + "description": "Optional pointer into buy-terms negotiation (forward-references the buy-terms namespace landing via separate RFC). Schema accepts any string for now and will tighten to a structured reference when the buy-terms RFC ships." + } + }, + "required": [ + "action", + "mode" + ], + "additionalProperties": false + }, + "uniqueItems": true + }, + "sandbox": { + "type": "boolean", + "description": "When true, this response contains simulated data from sandbox mode." + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "media_buy_id" + ], + "additionalProperties": true, + "not": { + "required": [ + "errors" + ] + } + }, + { + "title": "UpdateMediaBuyError", + "description": "Error response - operation failed, no changes applied", + "type": "object", + "properties": { + "errors": { + "type": "array", + "description": "Array of errors explaining why the operation failed", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "minItems": 1 + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "errors" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "media_buy_id" + ] + }, + { + "required": [ + "affected_packages" + ] + }, + { + "required": [ + "sandbox" + ] + }, + { + "properties": { + "status": { + "const": "submitted" + } + }, + "required": [ + "status" + ] + } + ] + } + }, + { + "title": "UpdateMediaBuySubmitted", + "description": "Async task envelope returned when update_media_buy cannot be confirmed before the response \u2014 for example, when operator re-approval is required for mid-flight changes. The buyer polls tasks/get with task_id or receives a webhook when the task completes; the updated media buy state lands on the completion artifact, not this envelope.", + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "submitted", + "description": "Task-level status literal. Discriminates this async envelope from the synchronous success shape, whose media_buy_id is issued in-line. See task-status.json for the full task-status enum." + }, + "task_id": { + "type": "string", + "description": "Task handle the buyer uses with tasks/get, and that the seller references on push-notification callbacks. Per AdCP wire conventions this is snake_case; A2A adapters MAY surface it as taskId, but the payload field emitted by the agent is task_id.", + "x-entity": "task" + }, + "message": { + "type": "string", + "maxLength": 2000, + "description": "Optional human-readable explanation of why the task is submitted \u2014 e.g., 'Awaiting operator re-approval; typical turnaround 2\u20134 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context \u2014 a hostile seller may inject prompt-injection payloads aimed at the buyer's agent." + }, + "errors": { + "type": "array", + "description": "Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "status", + "task_id" + ], + "additionalProperties": true, + "not": { + "required": [ + "media_buy_id" + ] + } + } + ], + "properties": {} + }, + { + "title": "Update Media Buy - Working", + "description": "Progress payload for active update_media_buy task.", + "type": "object", + "properties": { + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Completion percentage (0-100)" + }, + "current_step": { + "type": "string", + "description": "Current step or phase of the operation" + }, + "total_steps": { + "type": "integer", + "minimum": 1, + "description": "Total number of steps in the operation" + }, + "step_number": { + "type": "integer", + "minimum": 1, + "description": "Current step number" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + { + "title": "Update Media Buy - Input Required", + "description": "Payload when update_media_buy task is paused waiting for user input or approval.", + "type": "object", + "properties": { + "reason": { + "type": "string", + "enum": [ + "APPROVAL_REQUIRED", + "CHANGE_CONFIRMATION" + ], + "description": "Reason code indicating why input is needed" + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + }, + { + "title": "Update Media Buy - Submitted", + "description": "Async task envelope returned when update_media_buy cannot be confirmed before the response \u2014 for example, when operator re-approval is required for mid-flight changes. The buyer polls tasks/get with task_id or receives a webhook when the task completes; the updated media buy state lands on the completion artifact, not this envelope.", + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "submitted", + "description": "Task-level status literal. Discriminates this async envelope from the synchronous success shape, whose media_buy_id is issued in-line. See task-status.json for the full task-status enum." + }, + "task_id": { + "type": "string", + "description": "Task handle the buyer uses with tasks/get, and that the seller references on push-notification callbacks. Per AdCP wire conventions this is snake_case; A2A adapters MAY surface it as taskId, but the payload field emitted by the agent is task_id.", + "x-entity": "task" + }, + "message": { + "type": "string", + "maxLength": 2000, + "description": "Optional human-readable explanation of why the task is submitted \u2014 e.g., 'Awaiting operator re-approval; typical turnaround 2\u20134 hours.' Plain text only. Buyers MUST treat this as untrusted seller input: escape before rendering to HTML UIs, and sanitize or isolate before passing to an LLM prompt context \u2014 a hostile seller may inject prompt-injection payloads aimed at the buyer's agent." + }, + "errors": { + "type": "array", + "description": "Optional advisory errors accompanying the submitted envelope. Use only for non-blocking warnings (e.g., throttled_severity advisories, governance observations). Terminal failures belong in the error branch, not here.", + "items": { + "title": "Error", + "description": "Standard error structure for task-specific errors and warnings", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + } + }, + "context": { + "title": "Context Object", + "description": "Opaque correlation data that is echoed unchanged in responses. Used for internal tracking, UI session IDs, trace IDs, and other caller-specific identifiers that don't affect protocol behavior. Context data is never parsed by AdCP agents - it's simply preserved and returned.", + "type": "object", + "additionalProperties": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "status", + "task_id" + ], + "additionalProperties": true + }, + { + "title": "Build Creative Response", + "description": "Response payload for build_creative. Exactly one of four shapes: (1) synchronous single-format success \u2014 creative_manifest issued in-line (target_format_id request); (2) synchronous multi-format success \u2014 creative_manifests issued in-line (target_format_ids request); (3) terminal failure \u2014 an errors array; (4) submitted task envelope \u2014 status 'submitted' with task_id when the build is queued (e.g., slow generative or multi-minute LLM pipeline). The submitted branch MAY carry advisory errors for non-blocking warnings; terminal failures belong in the error branch. These four shapes are mutually exclusive \u2014 a response has exactly one.", + "type": "object", + "allOf": [ + { + "title": "AdCP Version Envelope", + "description": "Release-precision AdCP protocol version negotiation fields. Composed via `allOf` into every AdCP request and response schema so the version semantics live in exactly one place. Distinct from `core/protocol-envelope.json`, which wraps responses at the transport layer (context_id / task_id / status / payload). This envelope is part of the payload itself.", + "type": "object", + "properties": { + "adcp_version": { + "type": "string", + "description": "Release-precision AdCP version (VERSION.RELEASE, e.g. \"3.0\", \"3.1\", \"3.1-beta\"). On a request: the buyer's release pin \u2014 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 \u2014 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 \u2014 meta-field values are NOT valid wire values.", + "pattern": "^\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?$", + "examples": [ + "3.0", + "3.1", + "3.1-beta", + "3.1-rc.1" + ] + }, + "adcp_major_version": { + "type": "integer", + "description": "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.", + "minimum": 1, + "maximum": 99 + } + } + }, + { + "title": "Protocol Envelope", + "description": "Canonical envelope field-set for AdCP task responses, normalized across transports. Defines the protocol-layer fields (status, context_id, context, task_id, timestamp, replayed, adcp_error, push_notification_config, governance_context) and the conceptual `payload` grouping for task-specific response data. The serialization rules \u2014 whether envelope fields appear as siblings of payload fields, as a nested `payload` object, or via transport-native containers \u2014 are transport-specific and normative per transport (see Transport serialization below). The `status` field is REQUIRED on every task response envelope, including synchronous metadata responses (e.g., `get_adcp_capabilities`) where the value is `completed`. Agents shipping responses without a top-level `status` are non-conformant regardless of whether the task body schema would otherwise validate.", + "type": "object", + "properties": { + "context_id": { + "type": "string", + "description": "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": { + "title": "Context Object", + "description": "Per-request opaque caller-supplied correlation object echoed unchanged in the response. Used for buyer-side tracking (UI session IDs, trace IDs, custom metadata) that the agent MUST preserve byte-for-byte without parsing. Distinct from `context_id` (server-managed session identifier) \u2014 `context` is caller-owned echo, `context_id` is server-owned session scope. Both MAY appear on the same response.\n\n**Relationship to per-task body-level `context` declarations.** Many task request/response schemas (147 as of 3.1) already declare a body-level `context` field that `$ref`s `/schemas/core/context.json` at the body root. Under the flat-on-the-wire MCP serialization (see `notes` below), envelope-level `context` and body-level `context` occupy the same key on the response root \u2014 they are NOT separate fields, they MUST share the same value, and they MUST both `$ref` `core/context.json`. The envelope declaration is **authoritative** for the schema definition; per-task body declarations are mirrors retained for tooling reasons (SDK codegen completeness, per-task validation against the response schema in isolation). Future versions MAY drop body-level `context` declarations from per-task schemas; conformance does not require either declaration to be present, only that the wire value `$ref`s `core/context.json`.", + "type": "object", + "additionalProperties": true + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking asynchronous operations. Present when a task requires extended processing time. Used to query task status and retrieve results when complete.", + "x-entity": "task" + }, + "status": { + "$ref": "#/$defs/TaskStatus" + }, + "message": { + "type": "string", + "description": "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." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp when the response was generated. Useful for debugging, logging, cache validation, and tracking async operation progress." + }, + "replayed": { + "type": "boolean", + "description": "Set to true when this response was returned from the idempotency cache rather than from a fresh execution. Set to false (or omitted) when the request was executed fresh. Buyers use this to distinguish cached replays from new executions \u2014 matters for billing reconciliation, audit logs, state-machine routing (cached state-tracking fields are historical snapshots, not current state \u2014 re-read via the resource's read endpoint), and any downstream system that assumes exactly-once event semantics. From 3.1 onward, `replayed` MAY appear on responses to any request that resolved via the idempotency cache, including read tools \u2014 universal `idempotency_key` (see security.mdx \u00a7Idempotency) means the cache holds read responses too.", + "default": false + }, + "adcp_error": { + "title": "Error", + "description": "Transport-envelope error signal for fatal task failures. Per the two-layer model in `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`, a fatal task failure SHOULD populate both this envelope-level field AND the payload's `errors[]` array \u2014 the envelope carries a typed, extractable error so MCP/A2A clients can dispatch without re-parsing the payload, while the payload's structured `errors[]` remains the canonical normative shape. Non-fatal warnings populate ONLY `payload.errors[]` with `severity: warning` \u2014 the envelope MUST NOT carry `adcp_error` for non-failures.", + "type": "object", + "properties": { + "code": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Error code for programmatic handling. The error-code vocabulary is open: `error.code` is wire-typed `string` (not a closed enum), the standard codes published in `enums/error-code.json` are documentary, and senders MAY emit codes outside that set (platform-specific codes, or codes introduced in a later AdCP version). Receivers MUST decode unknown codes \u2014 treat the response as well-formed, read `error.recovery` for the recovery classification, and fall back to `transient` when `recovery` is absent. See `error-handling.mdx#forward-compatible-decoding-normative` for the full forward-compat contract \u2014 this rule is what lets future maintenance lines ship new codes additively." + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field path associated with the error in JSONPath-lite format (e.g., 'packages[0].targeting'). When `issues[]` is also present, sellers MUST set this to `issues[0].pointer` translated from RFC 6901 to JSONPath-lite (e.g., '/packages/0/targeting' \u2192 'packages[0].targeting') so pre-3.1 consumers reading `field` only get deterministic behavior. Will be deprecated in a future major version in favor of `issues[].pointer`." + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "retry_after": { + "type": "number", + "description": "Seconds to wait before retrying the operation. Sellers MUST return values between 1 and 3600. Clients MUST clamp values outside this range.", + "minimum": 1, + "maximum": 3600 + }, + "issues": { + "type": "array", + "description": "Structured list of validation failures. Primary use is `VALIDATION_ERROR`, where multi-field rejections are common and `field` (singular) cannot carry the full pointer map. MAY appear on other error codes that reject multiple fields at once. When `issues` is present, sellers MUST also populate `field` from `issues[0]` for backward compatibility with pre-3.1 consumers that read `field` only \u2014 translating the RFC 6901 `pointer` format to the JSONPath-lite format `field` uses (e.g., `/packages/0/targeting` \u2192 `packages[0].targeting`). MUST (not SHOULD) so consumers reading `field` get deterministic behavior across sellers \u2014 the cost is one line of dual-write per seller; the cost of SHOULD is a long tail of seller-A-vs-seller-B inconsistency. Future major versions will deprecate `field` in favor of `issues[].pointer`.", + "items": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "description": "RFC 6901 JSON Pointer to the offending field in the request payload (e.g., '/packages/0/targeting/geo_countries/2'). Format chosen to match Ajv's native validation output (`instancePath`); standardized and unambiguous on keys containing `/` or `~`. NOTE: this differs from the legacy top-level `field` which uses JSONPath-lite (`packages[0].targeting.geo_countries[2]`). When sellers populate `field` from `issues[0].pointer` for backward compatibility (see `field` description), they MUST translate the format \u2014 `/packages/0/x` \u2192 `packages[0].x`. Future major versions will deprecate `field` in favor of `issues[].pointer`." + }, + "message": { + "type": "string", + "description": "Human-readable description of why this specific field was rejected." + }, + "keyword": { + "type": "string", + "description": "Schema keyword that rejected the payload, drawn from the JSON Schema vocabulary (e.g., 'required', 'type', 'format', 'enum', 'pattern', 'minimum', 'maxLength'). Matches the keyword names emitted by JSON Schema validators (Ajv, jsonschema, etc.) so agents can pattern-match on rejection class without parsing message text. Implementers SHOULD use the validator's native keyword name; do not invent custom values here." + }, + "schemaPath": { + "type": "string", + "description": "Optional. JSON Schema tree path of the rejecting keyword (e.g. '#/properties/packages/items/oneOf/1'). 3.1+ consumers SHOULD prefer `schema_id`; `schemaPath` is retained for 3.0.x compatibility (renamed to `schema_path` in a future major). See error-handling.mdx for the validator-internals production-emit rules." + }, + "schema_id": { + "type": "string", + "description": "Optional. `$id` of the rejecting (sub-)schema (e.g. `/schemas/3.1.0/core/activation-key.json`). MUST resolve to a `$id` published in the spec at the version the seller advertises via `get_adcp_capabilities` \u2014 either a deep sub-schema (the typical case) or the response-root `$id` (the bundled-tree fallback for tools served from bundles built before #3868). Sellers MUST NOT emit when the rejection occurred against a private extension, server-only sub-schema, or pre-release element \u2014 the public-spec replay rationale only holds when the rejecting element is reachable from the public bundle. Sellers populating `schemaPath` SHOULD also populate `schema_id` when they have it so 3.1+ readers don't get strictly less than 3.0.x readers. See error-handling.mdx for resolution guidance and the bundled-tree caveat." + }, + "discriminator": { + "type": "array", + "description": "Optional. Const-discriminator property/value pair(s) identifying the variant the validator selected from values present in the payload. Sellers MUST populate only when (a) the rejecting schema is a const-discriminated `oneOf` / `anyOf` and (b) the discriminator property is present in the payload \u2014 emission on partial-match inference would fingerprint the seller's validator implementation. MUST omit when zero variants survive. Compound discriminators (e.g. `(type, value_type)`) produce multiple entries ordered by declaration in the rejecting schema's `properties` block. Same private-extensions / version-skew carve-out as `schema_id`. See error-handling.mdx.", + "items": { + "type": "object", + "properties": { + "property_name": { + "type": "string", + "description": "Discriminator property name (e.g., `type`, `value_type`). Aligns with OpenAPI 3.x `discriminator.propertyName`." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value the caller sent at `property_name`. Typically a string for const-discriminated unions; numeric/boolean/null permitted. Object and array values are forbidden \u2014 const discriminators are scalars, and emitting a structured value would conflate 'caller sent a complex shape' with 'validator inferred from a structural match'." + } + }, + "required": [ + "property_name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "pointer", + "message", + "keyword" + ], + "additionalProperties": true + } + }, + "details": { + "type": "object", + "description": "Additional task-specific error details. Sellers MAY mirror `issues[]` here as `details.issues` for backward compatibility with pre-3.1 consumers reading from `details`; new consumers SHOULD prefer the top-level `issues` field.\n\n**Canonical rejection-set shape (3.1+).** When the error reports a rejected value against a closed set of accepted values (e.g., enum mismatch, unsupported pricing option, invalid signal id), sellers SHOULD use the canonical key `accepted_values: ` under `details` rather than seller-specific variants observed in the wild (`available`, `allowed`, `accepted_values` at the error root, etc.). The canonical shape:\n\n```json\n{\n \"code\": \"INVALID_PRICING_MODEL\",\n \"message\": \"Pricing option not found: po_prism_abandoner_cpm\",\n \"field\": \"pricing_option_id\",\n \"details\": {\n \"rejected_value\": \"po_prism_abandoner_cpm\",\n \"accepted_values\": [\"po_prism_cart_cpm\", \"po_prism_view_cpm\"]\n }\n}\n```\n\n- `rejected_value` (optional): the offending value the buyer supplied, echoed for buyer-side diagnostic clarity (especially when the offending field is nested or transformed before validation).\n- `accepted_values` (optional): the closed set the seller would have accepted at this field on this call. Sellers MUST NOT enumerate the full ecosystem-wide accepted set if it differs from what's accepted for *this caller in this context* (account, brand, scope) \u2014 leaking ecosystem-wide accepted sets to a per-caller rejection turns the error into an enumeration oracle.\n\nThis is **SHOULD-level guidance**, not MUST: `details` remains `additionalProperties: true` and pre-3.1 sellers using `available` / `allowed` / `accepted_values` at the error root remain conformant. The canonical shape lets buyer-side diagnostic tooling (SDK runner hints, dashboards, error classifiers) reliably surface the accepted-set without per-seller pattern matching. SDKs SHOULD accept any of the legacy variants and normalize on read; the canonical shape is what new sellers and 3.1+ adopters should emit going forward.", + "additionalProperties": true + }, + "recovery": { + "type": "string", + "enum": [ + "transient", + "correctable", + "terminal" + ], + "description": "Agent recovery classification. transient: retry after delay (rate limit, service unavailable, timeout). correctable: fix the request and resend (invalid field, budget too low, creative rejected). terminal: requires human action (account suspended, payment required, account not found). Senders SHOULD populate `recovery` on every error from 3.1 onward \u2014 it is the normative carrier of recovery semantics across version skew. A receiver that does not recognize `error.code` (a newer code, or a platform-specific code) MUST still be able to classify the error from `recovery`. The `enumMetadata.recovery` block in `enums/error-code.json` is the documentary mirror for known codes; `error.recovery` on the wire is authoritative." + }, + "source": { + "type": "string", + "enum": [ + "producer", + "sdk" + ], + "description": "Who emitted this error entry. `producer` (default when absent): emitted by the response's authoring agent (the seller for `get_products`, the creative agent for `build_creative`, etc.). `sdk`: augmented by a consuming SDK that detected a non-fatal advisory condition on consumption (e.g., `FORMAT_PROJECTION_FAILED` when the buyer SDK couldn't project a v1 format to a canonical, or `FORMAT_DECLARATION_DIVERGENT` when the SDK detected a producer bug on read). SDK-augmented entries SHOULD also set `sdk_id` so downstream consumers can identify which intermediate processor inserted the entry.\n\n**Multi-hop propagation (normative).** AdCP is a federated agent network \u2014 responses commonly traverse multiple SDKs (e.g., sales agent \u2192 interchange \u2192 DSP \u2192 buyer). When an SDK augments `errors[]` with a consumption-detected entry, the augmented response carries the entry forward to subsequent hops. Each hop that detects the same condition independently SHOULD deduplicate by `(code, field)` rather than re-emit; the existing entry's `sdk_id` identifies which earlier processor saw it first. Producer entries (those without `source: \"sdk\"`) are authoritative for what the response's authoring agent self-detected; SDK entries are observations made on top.\n\n**Replay/audit safety.** Persisted or replayed responses carry `source` and `sdk_id` so the audit trail can distinguish seller-emitted entries from SDK-augmented ones. Without `source`, a downstream consumer can't tell whether a code came from the seller or an intermediate SDK, which corrupts attribution." + }, + "sdk_id": { + "type": "string", + "description": "Optional identifier for the SDK that augmented this error entry. Format: `@` (e.g., `@adcontextprotocol/adcp@7.3.0`, `adcontextprotocol-adcp-python@1.2.0`). MUST be set when `source: \"sdk\"`; MUST be absent when `source: \"producer\"` or absent. Lets downstream consumers identify which intermediate processor inserted the entry, useful for debugging cross-SDK divergence (e.g., one SDK detects a projection failure that another SDK's registry version doesn't)." + } + }, + "required": [ + "code", + "message" + ], + "additionalProperties": true + }, + "push_notification_config": { + "title": "Push Notification Config", + "description": "Push notification configuration for async task updates (A2A and REST protocols). Echoed from the request to confirm webhook settings. Specifies URL, authentication scheme (Bearer or HMAC-SHA256), and credentials. MCP uses progress notifications instead of webhooks.", + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Webhook endpoint URL for task status notifications. The wire contract is unconstrained beyond `format: \"uri\"` \u2014 in particular, publishers SHOULD NOT enforce a destination-port allowlist by default, since buyers legitimately host receivers on non-standard TLS ports (`:9443`, `:4443`, path-routed multi-tenant gateways). The SSRF guard the protocol relies on is the IP-range check + DNS-rebinding-resistant connect pin defined in [Webhook URL validation (SSRF)](/docs/building/by-layer/L1/security#webhook-url-validation-ssrf), not port filtering. Operators who want a hardened destination-port allowlist as defense-in-depth (e.g., locked-down enterprise egress) opt in explicitly \u2014 see [Destination port: permissive by default](/docs/building/by-layer/L1/security#destination-port-permissive-by-default)." + }, + "operation_id": { + "type": "string", + "description": "Buyer-supplied correlation identifier for the operation that will produce webhooks against this registration. The seller MUST echo this value verbatim into every webhook payload's `operation_id` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) and [Webhooks \u2014 Operation IDs](/docs/building/by-layer/L3/webhooks#operation-ids-and-url-templates)). Buyers SHOULD generate a unique value per task invocation (UUID recommended). This field is the canonical registration channel for `operation_id`; buyers MAY additionally embed the same value in the URL path or query as a routing aid for their own HTTP server, but the URL is opaque to the seller and the wire-level source of truth is this field. Sellers MUST NOT parse the URL to recover `operation_id`. Sellers that receive a webhook registration without `operation_id` MAY reject the task with `INVALID_REQUEST`.", + "minLength": 1, + "maxLength": 255, + "pattern": "^[A-Za-z0-9_.:-]{1,255}$" + }, + "token": { + "type": "string", + "description": "Optional client-provided token for webhook validation. The seller MUST echo this value verbatim in every webhook payload's `token` field (see [`mcp-webhook-payload.json`](/schemas/core/mcp-webhook-payload.json) for the receiver-side validation obligation). Length bounds give receivers a defensive range check on the echoed value; senders SHOULD generate tokens with at least 128 bits of entropy (\u226522 base64url characters). This is a complementary authenticity mechanism that can layer on top of the RFC 9421 webhook signature \u2014 unlike the `authentication` block below, it is not on the 4.0 removal track. Receivers that registered both a signing key (RFC 9421) and a `token` MUST NOT treat a valid token echo as authorization to skip signature verification; both checks remain independent obligations.", + "minLength": 16, + "maxLength": 4096 + }, + "authentication": { + "type": "object", + "description": "Legacy authentication configuration (A2A-compatible). Opts the seller into Bearer or HMAC-SHA256 signing instead of the default RFC 9421 webhook profile. Deprecated; removed in AdCP 4.0. **Precedence is a switch, not a fallback:** presence of this block selects the legacy scheme; absence selects 9421. A seller MUST NOT sign the same webhook both ways, and a buyer MUST NOT attempt 'try 9421 first, fall back to HMAC' verification \u2014 signature mode is determined solely by whether this block was present at registration time. The seller's baseline 9421 webhook-signing key published at its brand.json `agents[]` `jwks_uri` does not override this selector; it is always discoverable but only used when `authentication` is omitted. See docs/building/implementation/security.mdx#webhook-callbacks for the full precedence and downgrade-resistance rules (including the `webhook_mode_mismatch` rejection a buyer MUST apply when a received webhook's signing mode does not match the registered mode).", + "properties": { + "schemes": { + "type": "array", + "description": "Array of authentication schemes. Supported: ['Bearer'] for simple token auth, ['HMAC-SHA256'] for legacy shared-secret signing. Both are deprecated; new integrations SHOULD omit `authentication` and use the RFC 9421 webhook profile.", + "items": { + "$ref": "#/$defs/AuthenticationScheme" + }, + "minItems": 1, + "maxItems": 1 + }, + "credentials": { + "type": "string", + "description": "Credentials for the legacy scheme. For Bearer: token sent in Authorization header. For HMAC-SHA256: shared secret used to generate signature. Minimum 32 characters. Exchanged out-of-band during onboarding.", + "minLength": 32 + } + }, + "required": [ + "schemes", + "credentials" + ], + "additionalProperties": false + } + }, + "required": [ + "url" + ] + }, + "governance_context": { + "type": "string", + "description": "Governance context token issued by the account's governance agent during check_governance. Buyers attach it to governed purchase requests (media buys, rights acquisitions, signal activations, creative services); sellers persist it and include it on all subsequent governance calls for that action's lifecycle. An account binds to one governance agent (see sync_governance); governance is phased across `purchase` / `modification` / `delivery`, not partitioned across specialist agents, so the envelope carries a single token for the full lifecycle.\n\nValue format: governance agents MUST emit a compact JWS per the AdCP JWS profile (see Security \u2014 Signed Governance Context). Sellers MAY verify; sellers that do not verify MUST persist and forward the token unchanged. In 3.1 all sellers MUST verify. Non-JWS values from pre-3.0 governance agents are deprecated.\n\nThis is the primary correlation key for audit and reporting across the governance lifecycle.", + "minLength": 1, + "maxLength": 4096, + "pattern": "^[\\x20-\\x7E]+$" + }, + "payload": { + "type": "object", + "description": "Conceptual grouping for the task-specific response data defined by individual task response schemas (e.g., get-products-response.json, create-media-buy-response.json). `payload` is a documentary construct \u2014 it is NOT a required wire field, and its on-the-wire shape depends on transport (see Transport serialization below). Task response schemas declare body fields without wrapping them in a `payload` object; the wire representation places those body fields per transport convention. On MCP the body fields appear as siblings of envelope fields at the root of the tool response; on A2A they appear inside `task.artifacts[0].parts[].DataPart`; on REST they appear at the root of the JSON body.", + "additionalProperties": true + } + }, + "required": [ + "status" + ], + "additionalProperties": true, + "not": { + "anyOf": [ + { + "required": [ + "task_status" + ] + }, + { + "required": [ + "response_status" + ] + } + ] + }, + "examples": [ + { + "description": "Synchronous task response with immediate results", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Found 3 products matching your criteria for CTV inventory in California", + "timestamp": "2025-10-14T14:25:30Z", + "payload": { + "products": [ + { + "product_id": "ctv_premium_ca", + "name": "CTV Premium - California", + "description": "Premium connected TV inventory across California", + "pricing": { + "model": "cpm", + "amount": 45, + "currency": "USD" + } + } + ] + } + } + }, + { + "description": "Asynchronous task response with pending operation", + "data": { + "context_id": "ctx_def456", + "task_id": "task_789", + "status": "submitted", + "message": "Media buy creation submitted. Processing will take approximately 5-10 minutes. You'll receive updates via webhook.", + "timestamp": "2025-10-14T14:30:00Z", + "push_notification_config": { + "url": "https://buyer.example.com/webhooks/adcp", + "authentication": { + "schemes": [ + "HMAC-SHA256" + ], + "credentials": "shared_secret_exchanged_during_onboarding_min_32_chars" + } + }, + "payload": { + "account": { + "account_id": "acct_123" + } + } + } + }, + { + "description": "Task response requiring user input", + "data": { + "context_id": "ctx_ghi789", + "task_id": "task_101", + "status": "input-required", + "message": "This media buy requires manual approval. Please review the terms and confirm to proceed.", + "timestamp": "2025-10-14T14:32:15Z", + "payload": { + "media_buy_id": "mb_123456", + "packages": [ + { + "package_id": "pkg_001" + } + ], + "errors": [ + { + "code": "APPROVAL_REQUIRED", + "message": "Budget exceeds auto-approval threshold", + "severity": "warning" + } + ] + } + } + }, + { + "description": "Idempotent replay \u2014 same key and payload as a prior request within the replay window", + "data": { + "context_id": "ctx_abc123", + "status": "completed", + "message": "Returning cached response for idempotency_key (already processed)", + "timestamp": "2025-10-14T14:35:00Z", + "replayed": true, + "payload": { + "media_buy_id": "mb_01HW7J8K9P0Q1R2S3T4U5V6W7X" + } + } + }, + { + "description": "Failed task response with error details", + "data": { + "context_id": "ctx_jkl012", + "status": "failed", + "message": "Unable to create media buy due to invalid targeting parameters", + "timestamp": "2025-10-14T14:28:45Z", + "payload": { + "errors": [ + { + "code": "INVALID_TARGETING", + "message": "Geographic targeting codes are invalid", + "field": "targeting.geo_countries", + "severity": "error" + } + ] + } + } + } + ], + "notes": [ + "Task response schemas (e.g., get-products-response.json) define ONLY the body fields; protocol-layer fields live on this envelope.", + "Transport serialization (normative):", + " - MCP: envelope fields and task-body fields are siblings at the root of the tool response. The `payload` object is NOT serialized as a nested key \u2014 its body fields are flattened to the root alongside `status`, `context_id`, `context`, etc. This matches MCP's native `structuredContent` convention and is what shipping SDKs (@adcp/client) emit. Conformant MCP receivers parse from the flat root; receivers that expect a nested `payload` key MUST migrate.", + " - A2A (0.3.0+): envelope fields map to A2A's native task metadata (`task.status.state` carries `status`, `task.contextId` carries `context_id`, `task.id` carries `task_id`). Task-body fields are canonically carried in `task.artifacts[0].parts[].DataPart` on final states; `task.status.message.parts[].DataPart` is the fallback container used only for interim states (working, input-required) where no final artifact has been emitted yet. Receivers MUST prefer artifacts when present. See `a2a-response-extraction.mdx` for the full canonical/fallback algorithm.", + " - REST: envelope fields MAY ride on HTTP headers (e.g., `X-AdCP-Status`, `X-AdCP-Context-Id`) or as JSON body siblings; body fields appear at the JSON body root. Implementers choosing the header path SHOULD also mirror to body siblings for non-streaming callers.", + "Across all three: envelope and body fields are conceptually a single response object. A task response schema MAY declare body fields with the same name as envelope fields (e.g., `errors[]` body-level for per-record validation results vs envelope-level for fatal task failure) and the two MUST be treated as distinct fields by name within their respective namespaces \u2014 see `error-handling.mdx#envelope-vs-payload-errors-the-two-layer-model`.", + "`status` is REQUIRED on the conceptual envelope across all transports. On MCP and REST it appears as a sibling field at the JSON root (or `structuredContent` root for MCP); on A2A the canonical carrier is `task.status.state`, which maps 1:1 to this `status` value \u2014 receivers MUST extract A2A's `task.status.state` into the in-memory envelope `status` per the canonical extraction algorithm. The schema-level `required: [status]` enforces the post-extraction in-memory shape; the transport-native form satisfies the requirement on each wire. `payload` remains intentionally NOT required \u2014 it is a documentary grouping construct, never a required wire field. See `mcp-guide.mdx` and `a2a-guide.mdx` for the wire-level patterns receivers MUST implement.", + "Receivers MUST handle absence of an envelope field (e.g., `replayed` omitted) as the field's documented default \u2014 see each field's `default` clause." + ] + } + ], + "oneOf": [ + { + "title": "BuildCreativeSuccess", + "description": "Single-format success response. Returned when the request used target_format_id.", + "type": "object", + "properties": { + "creative_manifest": { + "title": "Creative Manifest", + "description": "The generated or transformed creative manifest", + "type": "object", + "properties": { + "format_id": { + "title": "Format Reference (Structured Object)", + "description": "Legacy named-format path. Always a structured object {agent_url, id} \u2014 never a plain string. Format identifier this manifest is for. Can be a template format (id only) or a deterministic format (id + dimensions/duration). For dimension-specific creatives, include width/height in the format_id to create a unique identifier (e.g., {id: 'display_static', width: 300, height: 250}). Mutually exclusive with `format_kind`.", + "x-entity": "creative_format", + "type": "object", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "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": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "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": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "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." + }, + "duration_ms": { + "type": "number", + "minimum": 1, + "description": "Duration in milliseconds for time-based formats (video, audio). When specified, creates a parameterized format ID. Omit to reference a template format without parameters." + } + }, + "required": [ + "agent_url", + "id" + ], + "additionalProperties": true, + "dependencies": { + "width": [ + "height" + ], + "height": [ + "width" + ] + } + }, + "format_kind": { + "$ref": "#/$defs/CanonicalFormatKind" + }, + "format_option_ref": { + "title": "Format Option Reference", + "description": "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 carries multiple `format_options` entries sharing the same `format_kind`; optional when `format_kind` alone routes the manifest to a single declaration. Product-scoped refs require an enclosing target product/package context.", + "type": "object", + "discriminator": { + "propertyName": "scope" + }, + "oneOf": [ + { + "title": "Publisher Catalog Format Option Reference", + "description": "Selects a publisher-catalog-backed product format option by publisher domain and format option ID.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "publisher", + "description": "Reference resolves against the named publisher's adagents.json top-level `formats[]` catalog." + }, + "publisher_domain": { + "type": "string", + "description": "Publisher domain where the adagents.json declaring this format option is hosted.", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" + }, + "format_option_id": { + "type": "string", + "description": "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[]`." + } + }, + "required": [ + "scope", + "publisher_domain", + "format_option_id" + ], + "additionalProperties": true + }, + { + "title": "Product-Local Format Option Reference", + "description": "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.", + "type": "object", + "properties": { + "scope": { + "type": "string", + "const": "product", + "description": "Reference resolves only against the target product's inline `format_options[]`." + }, + "format_option_id": { + "type": "string", + "description": "Stable format option ID from the target product's inline `format_options[]`." + }, + "publisher_domain": false + }, + "required": [ + "scope", + "format_option_id" + ], + "additionalProperties": true + } + ], + "additionalProperties": true + }, + "assets": { + "type": "object", + "description": "Map of slot keys to actual asset content. Legacy named-format path: each key matches an `asset_id` from the format's `assets` array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). 3.1+ canonical-format path: each key matches an `asset_group_id` from the format's `slots` declaration drawn from the canonical vocabulary registry (e.g., 'images_landscape', 'video', 'landing_page_url', 'vast_tag', 'script', 'creative_brief'). Either path produces the same envelope shape; only the slot-key vocabulary differs.\n\nEach slot value is **either** a single asset object (most slots \u2014 image, video, vast_tag, landing_page_url, etc.) **or** an array of asset objects (slots with `min`/`max` counts on the format declaration \u2014 `cards` on `image_carousel`, `headlines` / `descriptions` / `images_landscape` on `responsive_creative`, etc.). Single-vs-array shape is governed by the format's `slots[].min` and `slots[].max` parameters: when `max > 1` (or when the slot is conceptually a pool), the value MUST be an array; when the slot is single-valued, the value MUST be a single object. Each asset value (single or array element) carries an `asset_type` discriminator (image, video, audio, vast, daast, text, markdown, url, html, css, webhook, javascript, brief, catalog, zip, card) that selects the matching asset schema. Validators with OpenAPI-style discriminator support use `asset_type` to report errors against only the selected branch instead of all branches.", + "patternProperties": { + "^[a-z0-9_]+$": { + "oneOf": [ + { + "title": "AssetVariant", + "description": "Canonical union of all asset variant schemas. Referenced from creative-asset.json and creative-manifest.json to ensure a single named type is emitted by schema-to-TypeScript tooling. Add new asset types here and to the creative/asset-types registry.", + "oneOf": [ + { + "title": "Image Asset", + "description": "Image asset with URL and dimensions", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "image", + "description": "Discriminator identifying this as an image asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the image asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "format": { + "type": "string", + "description": "Image file format (jpg, png, gif, webp, etc.)" + }, + "alt_text": { + "type": "string", + "description": "Alternative text for accessibility", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + { + "title": "Video Asset", + "description": "Video asset with URL and technical specifications including audio track properties", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "video", + "description": "Discriminator identifying this as a video asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the video asset" + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels" + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels" + }, + "duration_ms": { + "type": "integer", + "description": "Video duration in milliseconds", + "minimum": 1 + }, + "file_size_bytes": { + "type": "integer", + "description": "File size in bytes", + "minimum": 1 + }, + "container_format": { + "type": "string", + "description": "Video container format (mp4, webm, mov, etc.)" + }, + "video_codec": { + "type": "string", + "description": "Video codec used (h264, h265, vp9, av1, prores, etc.)" + }, + "video_bitrate_kbps": { + "type": "integer", + "description": "Video stream bitrate in kilobits per second", + "minimum": 1 + }, + "frame_rate": { + "type": "string", + "description": "Frame rate as string to preserve precision (e.g., '23.976', '29.97', '30')" + }, + "frame_rate_type": { + "$ref": "#/$defs/FrameRateType" + }, + "scan_type": { + "$ref": "#/$defs/ScanType" + }, + "color_space": { + "type": "string", + "enum": [ + "rec709", + "rec2020", + "rec2100", + "srgb", + "dci_p3" + ], + "description": "Color space of the video" + }, + "hdr_format": { + "type": "string", + "enum": [ + "sdr", + "hdr10", + "hdr10_plus", + "hlg", + "dolby_vision" + ], + "description": "HDR format if applicable, or 'sdr' for standard dynamic range" + }, + "chroma_subsampling": { + "type": "string", + "enum": [ + "4:2:0", + "4:2:2", + "4:4:4" + ], + "description": "Chroma subsampling format" + }, + "video_bit_depth": { + "type": "integer", + "enum": [ + 8, + 10, + 12 + ], + "description": "Video bit depth" + }, + "gop_interval_seconds": { + "type": "number", + "description": "GOP/keyframe interval in seconds" + }, + "gop_type": { + "$ref": "#/$defs/GOPType" + }, + "moov_atom_position": { + "$ref": "#/$defs/MoovAtomPosition" + }, + "has_audio": { + "type": "boolean", + "description": "Whether the video contains an audio track" + }, + "audio_codec": { + "type": "string", + "description": "Audio codec used (aac, aac_lc, he_aac, pcm, mp3, ac3, eac3, etc.)" + }, + "audio_sampling_rate_hz": { + "type": "integer", + "description": "Audio sampling rate in Hz (e.g., 44100, 48000)" + }, + "audio_channels": { + "$ref": "#/$defs/AudioChannelLayout" + }, + "audio_bit_depth": { + "type": "integer", + "enum": [ + 16, + 24, + 32 + ], + "description": "Audio bit depth" + }, + "audio_bitrate_kbps": { + "type": "integer", + "description": "Audio bitrate in kilobits per second", + "minimum": 1 + }, + "audio_loudness_lufs": { + "type": "number", + "description": "Integrated loudness in LUFS" + }, + "audio_true_peak_dbfs": { + "type": "number", + "description": "True peak level in dBFS" + }, + "captions_url": { + "type": "string", + "format": "uri", + "description": "URL to captions file (WebVTT, SRT, etc.)", + "x-accessibility": true + }, + "transcript_url": { + "type": "string", + "format": "uri", + "description": "URL to text transcript of the video content", + "x-accessibility": true + }, + "audio_description_url": { + "type": "string", + "format": "uri", + "description": "URL to audio description track for visually impaired users", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url", + "width", + "height" + ], + "additionalProperties": true + }, + { + "title": "Audio Asset", + "description": "Audio asset with URL and technical specifications", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "audio", + "description": "Discriminator identifying this as an audio asset. See /schemas/creative/asset-types for the registry." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the audio asset" + }, + "duration_ms": { + "type": "integer", + "description": "Audio duration in milliseconds", + "minimum": 0 + }, + "file_size_bytes": { + "type": "integer", + "description": "File size in bytes", + "minimum": 1 + }, + "container_format": { + "type": "string", + "description": "Audio container/file format (mp3, m4a, aac, wav, ogg, flac, etc.)" + }, + "codec": { + "type": "string", + "description": "Audio codec used (aac, aac_lc, he_aac, pcm, mp3, vorbis, opus, flac, ac3, eac3, etc.)" + }, + "sampling_rate_hz": { + "type": "integer", + "description": "Sampling rate in Hz (e.g., 44100, 48000, 96000)" + }, + "channels": { + "$ref": "#/$defs/AudioChannelLayout" + }, + "bit_depth": { + "type": "integer", + "enum": [ + 16, + 24, + 32 + ], + "description": "Bit depth" + }, + "bitrate_kbps": { + "type": "integer", + "description": "Bitrate in kilobits per second", + "minimum": 1 + }, + "loudness_lufs": { + "type": "number", + "description": "Integrated loudness in LUFS" + }, + "true_peak_dbfs": { + "type": "number", + "description": "True peak level in dBFS" + }, + "transcript_url": { + "type": "string", + "format": "uri", + "description": "URL to text transcript of the audio content", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "url" + ], + "additionalProperties": true + }, + { + "title": "VAST Asset", + "description": "VAST (Video Ad Serving Template) tag for third-party video ad serving", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "vast", + "description": "Discriminator identifying this as a VAST asset. See /schemas/creative/asset-types for the registry." + }, + "vast_version": { + "$ref": "#/$defs/VASTVersion" + }, + "vpaid_enabled": { + "type": "boolean", + "description": "Whether VPAID (Video Player-Ad Interface Definition) is supported" + }, + "duration_ms": { + "type": "integer", + "description": "Expected video duration in milliseconds (if known)", + "minimum": 0 + }, + "tracking_events": { + "type": "array", + "items": { + "$ref": "#/$defs/VASTTrackingEvent" + }, + "description": "Tracking events supported by this VAST tag" + }, + "captions_url": { + "type": "string", + "format": "uri", + "description": "URL to captions file (WebVTT, SRT, etc.)", + "x-accessibility": true + }, + "audio_description_url": { + "type": "string", + "format": "uri", + "description": "URL to audio description track for visually impaired users", + "x-accessibility": true + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type" + ], + "oneOf": [ + { + "properties": { + "delivery_type": { + "type": "string", + "const": "url", + "description": "Discriminator indicating VAST is delivered via URL endpoint" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL endpoint that returns VAST XML" + } + }, + "required": [ + "delivery_type", + "url" + ] + }, + { + "properties": { + "delivery_type": { + "type": "string", + "const": "inline", + "description": "Discriminator indicating VAST is delivered as inline XML content" + }, + "content": { + "type": "string", + "description": "Inline VAST XML content" + } + }, + "required": [ + "delivery_type", + "content" + ] + } + ], + "discriminator": { + "propertyName": "delivery_type" + }, + "additionalProperties": true + }, + { + "title": "Text Asset", + "description": "Text content asset", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "const": "text", + "description": "Discriminator identifying this as a text asset. See /schemas/creative/asset-types for the registry." + }, + "content": { + "type": "string", + "description": "Text content" + }, + "language": { + "type": "string", + "description": "Language code (e.g., 'en', 'es', 'fr')" + }, + "provenance": { + "title": "Provenance", + "description": "Provenance metadata for this asset, overrides manifest-level provenance", + "type": "object", + "properties": { + "digital_source_type": { + "$ref": "#/$defs/DigitalSourceType" + }, + "ai_tool": { + "type": "object", + "description": "AI system used to generate or modify this content. Aligns with IPTC 2025.1 AI metadata fields and C2PA claim_generator.", + "properties": { + "name": { + "type": "string", + "description": "Name of the AI tool or model (e.g., 'DALL-E 3', 'Stable Diffusion XL', 'Gemini')" + }, + "version": { + "type": "string", + "description": "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." + }, + "provider": { + "type": "string", + "description": "Organization that provides the AI tool (e.g., 'OpenAI', 'Stability AI', 'Google')" + } + }, + "required": [ + "name" + ], + "additionalProperties": true + }, + "human_oversight": { + "type": "string", + "description": "Level of human involvement in the AI-assisted creation process. Independent of `disclosure.required` \u2014 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`.", + "enum": [ + "none", + "prompt_only", + "selected", + "edited", + "directed" + ], + "enumDescriptions": { + "none": "Fully automated with no human involvement in generation", + "prompt_only": "Human provided the prompt or instructions but did not review outputs", + "selected": "Human selected from multiple AI-generated candidates", + "edited": "Human edited or refined AI-generated output", + "directed": "Human directed the creative process with AI as an assistive tool" + } + }, + "declared_by": { + "type": "object", + "description": "Party declaring this provenance. Identifies who attached the provenance claim, enabling receiving parties to assess trust.", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL of the agent or service that declared this provenance" + }, + "role": { + "type": "string", + "enum": [ + "creator", + "advertiser", + "agency", + "platform", + "tool" + ], + "description": "Role of the declaring party in the supply chain", + "enumDescriptions": { + "creator": "The party that created or generated the content", + "advertiser": "The brand or advertiser that owns the content", + "agency": "Agency acting on behalf of the advertiser", + "platform": "Ad platform or publisher that processed the content", + "tool": "Automated tool or service that attached provenance metadata" + } + } + }, + "required": [ + "role" + ], + "additionalProperties": true + }, + "declared_at": { + "type": "string", + "format": "date-time", + "description": "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." + }, + "created_time": { + "type": "string", + "format": "date-time", + "description": "When this content was created or generated (ISO 8601)" + }, + "c2pa": { + "type": "object", + "description": "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.", + "properties": { + "manifest_url": { + "type": "string", + "format": "uri", + "description": "URL to the C2PA manifest store for this content" + } + }, + "required": [ + "manifest_url" + ], + "additionalProperties": true + }, + "embedded_provenance": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "method": { + "$ref": "#/$defs/EmbeddedProvenanceMethod" + }, + "standard": { + "type": "string", + "description": "Standard the embedding conforms to, if any (e.g., 'c2pa' for C2PA Section A.7 text manifest embedding)" + }, + "provider": { + "type": "string", + "description": "Organization that performed the embedding (e.g., 'Encypher', 'Digimarc'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the provenance data was embedded (ISO 8601)" + } + }, + "required": [ + "method", + "provider" + ], + "additionalProperties": true + } + }, + "watermarks": { + "type": "array", + "description": "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`).", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "media_type": { + "$ref": "#/$defs/WatermarkMediaType" + }, + "provider": { + "type": "string", + "description": "Organization that applied the watermark (e.g., 'Imatag', 'Steg.AI', 'Encypher'). Display label and audit context \u2014 not a wire identifier." + }, + "verify_agent": { + "type": "object", + "description": "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 \u2014 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).", + "properties": { + "agent_url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "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": { + "type": "string", + "description": "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." + } + }, + "required": [ + "agent_url" + ], + "additionalProperties": false + }, + "c2pa_action": { + "$ref": "#/$defs/C2PAWatermarkAction" + }, + "embedded_at": { + "type": "string", + "format": "date-time", + "description": "When the watermark was applied (ISO 8601)" + } + }, + "required": [ + "media_type", + "provider" + ], + "additionalProperties": true + } + }, + "disclosure": { + "type": "object", + "description": "Regulatory disclosure requirements for this content. Indicates whether AI disclosure is required and under which jurisdictions.", + "properties": { + "required": { + "type": "boolean", + "description": "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 \u2014 useful as a routing and audit input \u2014 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": { + "type": "array", + "description": "Jurisdictions where disclosure obligations apply", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code (e.g., 'US', 'DE', 'CN')" + }, + "region": { + "type": "string", + "description": "Sub-national region code (e.g., 'CA' for California, 'BY' for Bavaria)" + }, + "regulation": { + "type": "string", + "description": "Regulation identifier (e.g., 'eu_ai_act_article_50', 'ca_sb_942', 'cn_deep_synthesis')" + }, + "label_text": { + "type": "string", + "description": "Required disclosure label text for this jurisdiction, in the local language" + }, + "render_guidance": { + "type": "object", + "description": "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.", + "minProperties": 1, + "properties": { + "persistence": { + "$ref": "#/$defs/DisclosurePersistence" + }, + "min_duration_ms": { + "type": "integer", + "minimum": 1, + "description": "Minimum display duration in milliseconds for initial persistence. Recommended when persistence is initial \u2014 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." + }, + "positions": { + "type": "array", + "description": "Preferred disclosure positions in priority order. The first position a format supports should be used.", + "items": { + "$ref": "#/$defs/DisclosurePosition" + }, + "minItems": 1, + "uniqueItems": true + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "country", + "regulation" + ], + "additionalProperties": true + }, + "minItems": 1 + } + }, + "required": [ + "required" + ], + "additionalProperties": true + }, + "verification": { + "type": "array", + "description": "Third-party verification or detection results for this content. Multiple services may independently evaluate the same content. Provenance is a claim \u2014 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.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "verified_by": { + "type": "string", + "description": "Name of the verification service (e.g., 'DoubleVerify', 'Hive Moderation', 'Reality Defender')" + }, + "verified_time": { + "type": "string", + "format": "date-time", + "description": "When the verification was performed (ISO 8601)" + }, + "result": { + "type": "string", + "enum": [ + "authentic", + "ai_generated", + "ai_modified", + "inconclusive" + ], + "description": "Verification outcome" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score of the verification result (0.0 to 1.0)" + }, + "details_url": { + "type": "string", + "format": "uri", + "description": "URL to the full verification report" + } + }, + "required": [ + "verified_by", + "result" + ], + "additionalProperties": true + } + }, + "ext": { + "title": "Extension Object", + "description": "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.", + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true + } + }, + "required": [ + "asset_type", + "content" + ], + "additionalProperties": true + }, + { + "title": "URL Asset", + "$comment": "Fallback table is also rendered in docs/creative/asset-types.mdx (URL Asset section). Keep the two in sync \u2014 schema description is the normative source for conformance tools; mdx is the human-readable copy.", + "description": "URL reference asset. `url_type` declares the mechanism a receiver uses to invoke the URL (clickthrough vs. tracker_pixel vs. tracker_script) and is distinct from the URL's purpose, which the format declares in `url-asset-requirements.role` (clickthrough, landing_page, impression_tracker, click_tracker, viewability_tracker, third_party_tracker). Senders SHOULD include `url_type` on every URL asset. When `url_type` is absent, receivers SHOULD fall back to the format's `url-asset-requirements.role` per this mapping: clickthrough/landing_page \u2192 `clickthrough`; impression_tracker/click_tracker \u2192 `tracker_pixel`; viewability_tracker \u2192 `tracker_script` (OMID and equivalent verification SDKs require a