diff --git a/.changeset/assets-discovery.md b/.changeset/assets-discovery.md
new file mode 100644
index 00000000..43d94fd6
--- /dev/null
+++ b/.changeset/assets-discovery.md
@@ -0,0 +1,11 @@
+---
+"adcontextprotocol": minor
+---
+
+Add unified `assets` field to format schema for better asset discovery
+
+- Add new `assets` array to format schema with `required` boolean per asset
+- Deprecate `assets_required` (still supported for backward compatibility)
+- Enables full asset discovery for buyers and AI agents to see all supported assets
+- Optional assets like impression trackers can now be discovered and used
+
diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml
index b8b2bce7..8b3045af 100644
--- a/.github/workflows/broken-links.yml
+++ b/.github/workflows/broken-links.yml
@@ -2,9 +2,9 @@ name: Check for Broken Links
on:
push:
- branches: [ main, develop ]
+ branches: [ main, develop, '2.6.x' ]
pull_request:
- branches: [ main, develop ]
+ branches: [ main, develop, '2.6.x' ]
jobs:
broken-links:
diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml
index 346ed620..267de5f1 100644
--- a/.github/workflows/changeset-check.yml
+++ b/.github/workflows/changeset-check.yml
@@ -2,7 +2,7 @@ name: Changeset Check
on:
pull_request:
- branches: [main]
+ branches: [main, '2.6.x']
jobs:
check:
diff --git a/.github/workflows/check-testable-snippets.yml b/.github/workflows/check-testable-snippets.yml
index 7dea6c92..f28614b7 100644
--- a/.github/workflows/check-testable-snippets.yml
+++ b/.github/workflows/check-testable-snippets.yml
@@ -2,6 +2,7 @@ name: Check Testable Snippets
on:
pull_request:
+ branches: [main, '2.6.x']
paths:
- 'docs/**/*.md'
- 'docs/**/*.mdx'
@@ -26,7 +27,7 @@ jobs:
if [ -s changed_files.txt ]; then
echo "đź“‹ Checking documentation changes for testable snippets..."
- node scripts/check-testable-snippets.js
+ node scripts/check-testable-snippets.cjs
else
echo "âś“ No documentation files changed"
fi
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c5665ce6..176da7a2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
+ - '2.6.x'
concurrency: ${{ github.workflow }}-${{ github.ref }}
diff --git a/.github/workflows/schema-validation.yml b/.github/workflows/schema-validation.yml
index 7d772503..a599e1ad 100644
--- a/.github/workflows/schema-validation.yml
+++ b/.github/workflows/schema-validation.yml
@@ -2,9 +2,9 @@ name: JSON Schema Validation
on:
push:
- branches: [ main, develop ]
+ branches: [ main, develop, '2.6.x' ]
pull_request:
- branches: [ main, develop ]
+ branches: [ main, develop, '2.6.x' ]
jobs:
schema-validation:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ac14daf..82e40e15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,44 @@
# Changelog
+## 2.6.0
+
+### Minor Changes
+
+- Add unified `assets` field to format schema for better asset discovery
+
+ **Schema Changes:**
+
+ - **format.json**: Add new `assets` array field that includes both required and optional assets
+ - **format.json**: Deprecate `assets_required` (still supported for backward compatibility)
+
+ **Rationale:**
+
+ Previously, buyers and AI agents could only see required assets via `assets_required`. There was no way to discover optional assets that enhance creatives (companion banners, third-party tracking pixels, etc.).
+
+ Since each asset already has a `required` boolean field, we introduced a unified `assets` array where:
+ - `required: true` - Asset MUST be provided for a valid creative
+ - `required: false` - Asset is optional, enhances the creative when provided
+
+ This enables:
+ - **Full asset discovery**: Buyers and AI agents can see ALL assets a format supports
+ - **Richer creatives**: Optional assets like impression trackers can now be discovered and used
+ - **Cleaner schema**: Single array instead of two separate arrays
+
+ **Example:**
+
+ ```json
+ {
+ "format_id": { "agent_url": "https://creative.adcontextprotocol.org", "id": "video_30s" },
+ "assets": [
+ { "item_type": "individual", "asset_id": "video_file", "asset_type": "video", "required": true },
+ { "item_type": "individual", "asset_id": "end_card", "asset_type": "image", "required": false },
+ { "item_type": "individual", "asset_id": "impression_tracker", "asset_type": "url", "required": false }
+ ]
+ }
+ ```
+
+ **Migration:** Non-breaking change. `assets_required` is deprecated but still supported. New implementations should use `assets`.
+
## 2.5.1
### Patch Changes
diff --git a/docs/creative/asset-types.mdx b/docs/creative/asset-types.mdx
index c3d58aa6..927fbedf 100644
--- a/docs/creative/asset-types.mdx
+++ b/docs/creative/asset-types.mdx
@@ -338,7 +338,9 @@ The keys in the assets object correspond to the `asset_id` values defined in the
## Usage in Creative Formats
-Creative formats specify their required assets using `assets_required` (an array):
+Creative formats specify their assets using the `assets` array. Each asset has a `required` boolean:
+- **`required: true`** - Asset MUST be provided for a valid creative
+- **`required: false`** - Asset is optional, enhances the creative (e.g., companion banners, third-party tracking pixels)
```json
{
@@ -346,7 +348,7 @@ Creative formats specify their required assets using `assets_required` (an array
"agent_url": "https://creative.adcontextprotocol.org",
"id": "video_15s_hosted"
},
- "assets_required": [
+ "assets": [
{
"item_type": "individual",
"asset_id": "video_file",
@@ -359,11 +361,41 @@ Creative formats specify their required assets using `assets_required` (an array
"acceptable_resolutions": ["1920x1080", "1280x720"],
"max_file_size_mb": 30
}
+ },
+ {
+ "item_type": "individual",
+ "asset_id": "impression_tracker",
+ "asset_type": "url",
+ "required": false,
+ "requirements": {
+ "format": ["url"],
+ "description": "Third-party impression tracking pixel URL"
+ }
+ }
+ ],
+
+ // DEPRECATED: Use "assets" above instead. Kept for backward compatibility.
+ "assets_required": [
+ {
+ "item_type": "individual",
+ "asset_id": "video_file",
+ "asset_type": "video",
+ "requirements": {
+ "duration_seconds": 15,
+ "acceptable_formats": ["mp4"],
+ "acceptable_codecs": ["h264"],
+ "acceptable_resolutions": ["1920x1080", "1280x720"],
+ "max_file_size_mb": 30
+ }
}
]
}
```
+
+**Backward Compatibility**: The `assets_required` field is deprecated but still supported. Existing implementations can continue using `assets_required` for required assets only. New implementations should use `assets` with the `required` boolean for full asset discovery.
+
+
## Repeatable Asset Groups
For formats with asset sequences (like carousels, slideshows, stories), see the [Carousel & Multi-Asset Formats guide](/docs/creative/channels/carousels) for complete documentation on repeatable asset group patterns.
diff --git a/docs/creative/formats.mdx b/docs/creative/formats.mdx
index 54ae1f1c..ba6b100d 100644
--- a/docs/creative/formats.mdx
+++ b/docs/creative/formats.mdx
@@ -222,7 +222,7 @@ Formats are JSON objects with the following key fields:
},
"name": "30-Second Hosted Video",
"type": "video",
- "assets_required": [
+ "assets": [
{
"item_type": "individual",
"asset_id": "video_file",
@@ -234,6 +234,53 @@ Formats are JSON objects with the following key fields:
"format": ["MP4"],
"resolution": ["1920x1080", "1280x720"]
}
+ },
+ {
+ "item_type": "individual",
+ "asset_id": "end_card_image",
+ "asset_type": "image",
+ "asset_role": "end_card",
+ "required": false,
+ "requirements": {
+ "dimensions": "1920x1080",
+ "format": ["PNG", "JPG"]
+ }
+ },
+ {
+ "item_type": "individual",
+ "asset_id": "companion_banner",
+ "asset_type": "image",
+ "asset_role": "companion",
+ "required": false,
+ "requirements": {
+ "dimensions": "300x250",
+ "format": ["PNG", "JPG", "GIF"]
+ }
+ },
+ {
+ "item_type": "individual",
+ "asset_id": "impression_tracker",
+ "asset_type": "url",
+ "asset_role": "third_party_tracking",
+ "required": false,
+ "requirements": {
+ "description": "Third-party impression tracking pixel URL"
+ }
+ }
+ ],
+
+ // DEPRECATED: Use "assets" above instead. Kept for backward compatibility.
+ "assets_required": [
+ {
+ "item_type": "individual",
+ "asset_id": "video_file",
+ "asset_type": "video",
+ "asset_role": "hero_video",
+ "requirements": {
+ "duration": "30s",
+ "format": ["MP4"],
+ "resolution": ["1920x1080", "1280x720"]
+ }
}
]
}
@@ -243,10 +290,24 @@ Formats are JSON objects with the following key fields:
- **format_id**: Unique identifier (may be namespaced with `domain:id`)
- **agent_url**: The creative agent authoritative for this format
- **type**: Category (video, display, audio, native, dooh, rich_media)
-- **assets_required**: Array of asset specifications
+- **assets**: Array of all asset specifications with `required` boolean indicating mandatory vs optional
+- **assets_required**: *(Deprecated)* Array of required asset specifications - use `assets` instead
- **asset_role**: Identifies asset purpose (hero_image, logo, cta_button, etc.)
- **renders**: Array of rendered outputs with dimensions - see below
+### Asset Discovery
+
+The `assets` array enables comprehensive asset discovery. Each asset has a `required` boolean:
+
+- **`required: true`** - Asset MUST be provided for a valid creative
+- **`required: false`** - Asset is optional, enhances the creative when provided (e.g., companion banners, third-party tracking pixels)
+
+This unified approach helps creative tools and AI agents understand the full capabilities of a format, enabling richer creative experiences when optional assets are available while clearly indicating minimum requirements.
+
+
+**Backward Compatibility**: The `assets_required` field is deprecated but still supported. Existing implementations can continue using `assets_required` for required assets only. New implementations should use `assets` with the `required` boolean on each asset for full asset discovery.
+
+
### Rendered Outputs and Dimensions
Formats specify their rendered outputs via the `renders` array. Most formats produce a single render, but some (companion ads, adaptive formats, multi-placement) produce multiple renders:
diff --git a/docs/creative/implementing-creative-agents.mdx b/docs/creative/implementing-creative-agents.mdx
index 98671f1e..abb86bb5 100644
--- a/docs/creative/implementing-creative-agents.mdx
+++ b/docs/creative/implementing-creative-agents.mdx
@@ -100,7 +100,7 @@ Creative agents must implement these two tasks:
Return all formats your agent defines. This is how buyers discover what creative formats you support.
**Key responsibilities:**
-- Return complete format definitions with all `assets_required`
+- Return complete format definitions with all `assets` (both required and optional)
- Include your `agent_url` in each format
- Use proper namespacing for `format_id` values
@@ -169,7 +169,7 @@ When validating manifests:
When updating format definitions:
-- **Additive changes** (new optional assets) are safe
+- **Additive changes** (new optional assets with `required: false` in `assets`) are safe
- **Breaking changes** (removing assets, changing requirements) require new format_id
- Consider versioning: `youragency.com:format_name_v2`
- Maintain backward compatibility when possible
diff --git a/docs/creative/task-reference/list_creative_formats.mdx b/docs/creative/task-reference/list_creative_formats.mdx
index 648c6f67..af0348f7 100644
--- a/docs/creative/task-reference/list_creative_formats.mdx
+++ b/docs/creative/task-reference/list_creative_formats.mdx
@@ -56,7 +56,7 @@ Formats may produce multiple rendered pieces (e.g., video + companion banner). D
| Field | Description |
|-------|-------------|
-| `formats` | Array of full format definitions (format_id, name, type, requirements, assets_required, renders) |
+| `formats` | Array of full format definitions (format_id, name, type, requirements, assets, assets_required, renders) |
| `creative_agents` | Optional array of other creative agents providing additional formats |
See [Format schema](https://adcontextprotocol.org/schemas/v2/core/format.json) for complete format object structure.
@@ -421,7 +421,8 @@ Each format includes:
| `name` | Human-readable format name |
| `type` | Format type (audio, video, display, dooh) |
| `requirements` | Technical requirements (duration, file types, bitrate, etc.) |
-| `assets_required` | Array of required assets with specifications |
+| `assets` | Array of all assets with `required` boolean indicating mandatory vs optional |
+| `assets_required` | *(Deprecated)* Array of required assets - use `assets` instead |
| `renders` | Array of rendered output pieces (dimensions, role) |
### Asset Roles
diff --git a/package-lock.json b/package-lock.json
index c249c1f9..9218d630 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "adcontextprotocol",
- "version": "2.5.1",
+ "version": "2.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "adcontextprotocol",
- "version": "2.5.1",
+ "version": "2.6.0",
"dependencies": {
"@adcp/client": "^3.4.0",
"@anthropic-ai/sdk": "^0.71.2",
@@ -12010,7 +12010,7 @@
"license": "MIT"
},
"node_modules/fast-memoize": {
- "version": "2.5.2",
+ "version": "2.6.0",
"resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
"integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==",
"dev": true,
@@ -21123,6 +21123,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/standardwebhooks": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
+ "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
+ "license": "MIT",
+ "dependencies": {
+ "@stablelib/base64": "^1.0.0",
+ "fast-sha256": "^1.3.0"
+ }
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -21557,13 +21567,12 @@
}
},
"node_modules/svix": {
- "version": "1.82.0",
- "resolved": "https://registry.npmjs.org/svix/-/svix-1.82.0.tgz",
- "integrity": "sha512-K2M7yFSzuwJVPxi2/I5R9STofIQ8gO4PZ+ptZ5RB+zhTNHO12UtYk+uuuA2wIQ4wCj3GYY1WhvKYDeYqptIwKg==",
+ "version": "1.84.1",
+ "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
+ "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
"license": "MIT",
"dependencies": {
- "@stablelib/base64": "^1.0.0",
- "fast-sha256": "^1.3.0",
+ "standardwebhooks": "1.0.0",
"uuid": "^10.0.0"
}
},
diff --git a/package.json b/package.json
index 58c04491..3a3d4aa1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "adcontextprotocol",
- "version": "2.5.1",
+ "version": "2.6.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/server/src/http.ts b/server/src/http.ts
index 0f46048f..5cb4fe83 100644
--- a/server/src/http.ts
+++ b/server/src/http.ts
@@ -5956,18 +5956,23 @@ Disallow: /api/admin/
return res.json({
success: true,
- formats: formats.map(format => ({
- format_id: format.format_id,
- name: format.name,
- type: format.type,
- description: format.description,
- preview_image: format.preview_image,
- example_url: format.example_url,
- renders: format.renders,
- assets_required: format.assets_required,
- output_format_ids: format.output_format_ids,
- agent_url: format.agent_url,
- })),
+ formats: formats.map(format => {
+ // Cast to allow 'assets' field (added in schema v2.5.2, @adcp/client may not have it yet)
+ const formatWithAssets = format as typeof format & { assets?: unknown };
+ return {
+ format_id: format.format_id,
+ name: format.name,
+ type: format.type,
+ description: format.description,
+ preview_image: format.preview_image,
+ example_url: format.example_url,
+ renders: format.renders,
+ assets_required: format.assets_required, // deprecated but kept for backward compatibility
+ assets: formatWithAssets.assets, // new unified field
+ output_format_ids: format.output_format_ids,
+ agent_url: format.agent_url,
+ };
+ }),
});
} catch (error) {
logger.error({ err: error, url }, 'Agent formats fetch error');
diff --git a/static/schemas/source/core/format.json b/static/schemas/source/core/format.json
index d778d5de..a018252d 100644
--- a/static/schemas/source/core/format.json
+++ b/static/schemas/source/core/format.json
@@ -121,7 +121,8 @@
},
"assets_required": {
"type": "array",
- "description": "Array of required assets or asset groups for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Can contain individual assets or repeatable asset sequences (e.g., carousel products, slideshow frames).",
+ "deprecated": true,
+ "description": "DEPRECATED: Use 'assets' instead. Array of required assets or asset groups for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Can contain individual assets or repeatable asset sequences (e.g., carousel products, slideshow frames). This field is maintained for backward compatibility; new implementations should use 'assets' with the 'required' boolean on each asset.",
"items": {
"oneOf": [
{
@@ -217,6 +218,109 @@
]
}
},
+ "assets": {
+ "type": "array",
+ "description": "Array of all assets supported for this format. Each asset is identified by its asset_id, which must be used as the key in creative manifests. Use the 'required' boolean on each asset to indicate whether it's mandatory. This field replaces the deprecated 'assets_required' and enables full asset discovery for buyers and AI agents.",
+ "items": {
+ "oneOf": [
+ {
+ "description": "Individual asset specification",
+ "type": "object",
+ "properties": {
+ "item_type": {
+ "type": "string",
+ "const": "individual",
+ "description": "Discriminator indicating this is an individual asset"
+ },
+ "asset_id": {
+ "type": "string",
+ "description": "Unique identifier for this asset. Creative manifests MUST use this exact value as the key in the assets object."
+ },
+ "asset_type": {
+ "$ref": "/schemas/enums/asset-content-type.json",
+ "description": "Type of asset"
+ },
+ "asset_role": {
+ "type": "string",
+ "description": "Optional descriptive label for this asset's purpose (e.g., 'hero_image', 'logo', 'third_party_tracking'). Not used for referencing assets in manifests—use asset_id instead. This field is for human-readable documentation and UI display only."
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether this asset is required (true) or optional (false). Required assets must be provided for a valid creative. Optional assets enhance the creative but are not mandatory."
+ },
+ "requirements": {
+ "type": "object",
+ "description": "Technical requirements for this asset (dimensions, file size, duration, etc.). For template formats, use parameters_from_format_id: true to indicate asset parameters must match the format_id parameters (width/height/unit and/or duration_ms).",
+ "additionalProperties": true
+ }
+ },
+ "required": ["item_type", "asset_id", "asset_type", "required"]
+ },
+ {
+ "description": "Repeatable asset group (for carousels, slideshows, playlists, etc.)",
+ "type": "object",
+ "properties": {
+ "item_type": {
+ "type": "string",
+ "const": "repeatable_group",
+ "description": "Discriminator indicating this is a repeatable asset group"
+ },
+ "asset_group_id": {
+ "type": "string",
+ "description": "Identifier for this asset group (e.g., 'product', 'slide', 'card')"
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether this asset group is required. If true, at least min_count repetitions must be provided."
+ },
+ "min_count": {
+ "type": "integer",
+ "description": "Minimum number of repetitions required (if group is required) or allowed (if optional)",
+ "minimum": 0
+ },
+ "max_count": {
+ "type": "integer",
+ "description": "Maximum number of repetitions allowed",
+ "minimum": 1
+ },
+ "assets": {
+ "type": "array",
+ "description": "Assets within each repetition of this group",
+ "items": {
+ "type": "object",
+ "properties": {
+ "asset_id": {
+ "type": "string",
+ "description": "Identifier for this asset within the group"
+ },
+ "asset_type": {
+ "$ref": "/schemas/enums/asset-content-type.json",
+ "description": "Type of asset"
+ },
+ "asset_role": {
+ "type": "string",
+ "description": "Optional descriptive label for this asset's purpose. Not used for referencing assets in manifests—use asset_id instead. This field is for human-readable documentation and UI display only."
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether this asset is required within each repetition of the group",
+ "default": false
+ },
+ "requirements": {
+ "type": "object",
+ "description": "Technical requirements for this asset. For template formats, use parameters_from_format_id: true to indicate asset parameters must match the format_id parameters (width/height/unit and/or duration_ms).",
+ "additionalProperties": true
+ }
+ },
+ "required": ["asset_id", "asset_type", "required"]
+ }
+ }
+ },
+ "required": ["item_type", "asset_group_id", "required", "min_count", "max_count", "assets"]
+ }
+ ]
+ }
+ },
"delivery": {
"type": "object",
"description": "Delivery method specifications (e.g., hosted, VAST, third-party tags)",
diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json
index 2056c90c..6ea9e42a 100644
--- a/static/schemas/source/index.json
+++ b/static/schemas/source/index.json
@@ -4,7 +4,7 @@
"title": "AdCP Schema Registry v1",
"version": "1.0.0",
"description": "Registry of all AdCP JSON schemas for validation and discovery",
- "adcp_version": "2.5.1",
+ "adcp_version": "2.6.0",
"standard_formats_version": "2.0.0",
"versioning": {
"note": "AdCP uses path-based versioning. The schema URL path (/schemas/) indicates the version. Individual request/response schemas do NOT include adcp_version fields. Compatibility follows semantic versioning rules."