Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/feat-vast-validation-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"adcontextprotocol": minor
---

Define a normative VAST validation contract for `vast` creative assets. Today the entire format-layer contract for a VAST asset is the `vast_version` string in `vast-asset-requirements.json`: nothing in the spec requires parsing the document, checking for an `<Ad>` or `<MediaFile>`, verifying the version attribute, or bounding wrapper chains, and `error-code.json` has no VAST codes, so a structurally valid manifest can carry an unplayable tag that fails silently at serve time.

This change adds:

- `creative_specs.vast_validation` on `get_adcp_capabilities` (`structural` | `document` | `wrapper`, default `structural`), following the capability-gating pattern of `media_buy.governance_aware`: sellers that do not inspect VAST documents keep the default and are unaffected.
- A "VAST Validation" section in the video channel docs specifying the checks at each level: document parse, root element and `<VAST version>` agreement with the declared `vast_version` / format requirement / seller `vast_versions`, `<Ad>` and `<MediaFile>` presence, HTTPS URLs, wrapper resolution bounded by the format's existing `max_wrapper_depth`, loop detection, per-hop timeout, and terminal-document checks. Validation runs at `sync_creatives` (including `dry_run`); `validate_input` stays manifest-structure-only. A passing preflight is explicitly not approval of future responses from a decisioning endpoint.
- Three error codes with `enumDescriptions` and `enumMetadata`: `VAST_PARSE_FAILED`, `VAST_VERSION_MISMATCH`, `VAST_WRAPPER_DEPTH_EXCEEDED` (all correctable, with `error.details.reason` discriminators).

Macro correctness and substitution verification are explicitly out of scope (in-flight WG work on click-tracker insertion and decisioning-time substitution). Additive; no change for sellers that do not declare the capability.
38 changes: 38 additions & 0 deletions docs/creative/channels/video.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,44 @@ AdCP supports all VAST versions:
- **VAST 4.1** - Enhanced ad pod support
- **VAST 4.2** - Latest specification (recommended)

### VAST Validation

Format-layer validation of `vast` assets is structural: the manifest must satisfy the format's `requirements` (today, `vast_version`), and [`validate_input`](/docs/creative/canonical-formats#validation-flow--validate_input) checks manifest shape only. The VAST document itself is opaque at that level, so a structurally valid manifest can still carry an unplayable tag that fails silently at serve time.

AdCP defines three validation levels for `vast` assets. Sellers declare the level they perform via `creative_specs.vast_validation` in [`get_adcp_capabilities`](/docs/protocol/get_adcp_capabilities); absent means `structural`. Validation runs at `sync_creatives` time, including `dry_run`. `validate_input` remains manifest-structure-only at every level.

| Level | What the seller checks |
|-------|------------------------|
| `structural` (default) | Manifest shape and format `requirements` only. The VAST document is not inspected. |
| `document` | Everything in `structural`, plus the VAST document itself. |
| `wrapper` | Everything in `document`, plus wrapper-chain resolution. |

**Document-level validation.** Sellers declaring `document` or `wrapper`:

- MUST parse the document (inline `content`, or one response fetched from `url`) as XML and reject unparseable documents with `VAST_PARSE_FAILED` (`error.details.reason: "not_xml"`).
- MUST reject documents whose root element is not `<VAST>` with `VAST_PARSE_FAILED` (`error.details.reason: "no_vast_root"`).
- MUST reject documents whose `<VAST version>` attribute is missing or does not match the asset's declared `vast_version`, the format's `vast_version` requirement, or the seller's declared `creative_specs.vast_versions`, with `VAST_VERSION_MISMATCH` (`error.details.supported_versions` lists the accepted versions).
- SHOULD reject documents containing no `<Ad>` element, and `<InLine>` linear creatives carrying no `<MediaFile>`, with `VAST_PARSE_FAILED` (`error.details.reason: "no_ad"` / `"no_media_file"`).
- SHOULD verify that media, tracking, and click URLs use HTTPS.
- MUST treat unresolved ad-server macros in URLs (`[MACRO]`, `${MACRO}`, `{UNIVERSAL_MACRO}`) as opaque tokens, never as validation failures. Macro correctness and substitution verification are out of scope for this contract.

**Wrapper-chain validation.** Sellers declaring `wrapper` additionally:

- MUST resolve `<Wrapper>` redirects via `<VASTAdTagURI>` up to the format's declared `max_wrapper_depth` (when present) and reject deeper chains with `VAST_WRAPPER_DEPTH_EXCEEDED` (`error.details.reason: "depth"`).
- MUST reject chains that revisit a `<VASTAdTagURI>` already seen (a loop) with `VAST_WRAPPER_DEPTH_EXCEEDED` (`error.details.reason: "loop"`).
- SHOULD apply a per-hop timeout, rejecting with `VAST_WRAPPER_DEPTH_EXCEEDED` (`error.details.reason: "timeout"`), and MUST fetch wrapper hops over HTTPS only.
- MUST apply document-level checks to the terminal `<InLine>` document and reject with the corresponding document-level code on failure.

**Preflight, not approval.** A VAST endpoint can return a different document per request (decisioning, rotation, macros). Passing validation at sync time is a preflight of the tag's shape at that moment, not a guarantee about future responses. Sellers MUST NOT treat a passing preflight as approval of all future responses and MAY re-validate during delivery. Sellers fetching `url` assets or wrapper hops SHOULD apply the same server-side request protections (private-address blocking, redirect and size limits) they apply to any other agent-supplied URL fetch.

**Error codes.** In all cases `error.field` identifies the offending asset path in the manifest.

| Code | Meaning |
|------|---------|
| `VAST_PARSE_FAILED` | Document is not well-formed XML, has no `<VAST>` root, no `<Ad>`, or an inline linear creative has no `<MediaFile>` |
| `VAST_VERSION_MISMATCH` | Document's `<VAST version>` disagrees with the declared `vast_version`, the format requirement, or the seller's supported versions |
| `VAST_WRAPPER_DEPTH_EXCEEDED` | Wrapper chain exceeds `max_wrapper_depth`, loops, or a hop timed out |

### VPAID Support

VPAID (Video Player Ad-Serving Interface Definition) enables interactive video ads:
Expand Down
2 changes: 2 additions & 0 deletions docs/creative/task-reference/sync_creatives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,8 @@ Poll `tasks/get` or wait for the webhook. The completion artifact carries the `c
| `FORMAT_MISMATCH` | Assets don't match format requirements | Verify asset types and specifications match format definition |
| `CREATIVE_IN_ACTIVE_DELIVERY` | Creative is assigned to an active, non-paused package (blocks updates and `delete_missing` deletions) | Pause the package first, or create a new creative version |

Sellers that declare `creative_specs.vast_validation` of `document` or `wrapper` additionally validate `vast` assets at sync time (including `dry_run`) and can return `VAST_PARSE_FAILED`, `VAST_VERSION_MISMATCH`, or `VAST_WRAPPER_DEPTH_EXCEEDED`. See [VAST Validation](/docs/creative/channels/video#vast-validation).

## Best practices

1. **Use upsert semantics** - Same `creative_id` updates existing creative rather than creating duplicates. This allows iterative creative development. Note: updates are blocked for creatives in active delivery (see #7).
Expand Down
15 changes: 15 additions & 0 deletions scripts/error-code-drift-dispositions.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@
"disposition": "held-for-next-minor",
"target_version": "3.1",
"note": "From sync_accounts polymorphic-key refactor (#2261). Returned per-entry when the seller does not implement the entry's `sync_accounts` mode — settings-update sellers (explicit-account platforms) reject natural-key entries; provisioning-only sellers reject AccountRef-keyed entries. Wire change — held for 3.1."
},
"VAST_PARSE_FAILED": {
"disposition": "held-for-next-minor",
"target_version": "3.2",
"note": "VAST validation contract: document-level validation of vast assets at sync_creatives for sellers declaring creative_specs.vast_validation of document or wrapper. New VAST-validation surface; wire change, held for 3.2."
},
"VAST_VERSION_MISMATCH": {
"disposition": "held-for-next-minor",
"target_version": "3.2",
"note": "VAST validation contract: the VAST document's version attribute disagrees with the declared vast_version, the format requirement, or the seller's vast_versions. New VAST-validation surface; wire change, held for 3.2."
},
"VAST_WRAPPER_DEPTH_EXCEEDED": {
"disposition": "held-for-next-minor",
"target_version": "3.2",
"note": "VAST validation contract: wrapper-chain resolution exceeded max_wrapper_depth, looped, or timed out, for sellers declaring creative_specs.vast_validation: wrapper. New VAST-validation surface; wire change, held for 3.2."
}
}
}
22 changes: 20 additions & 2 deletions static/schemas/source/enums/error-code.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@
"FEED_FETCH_FAILED",
"INVALID_FEED_FORMAT",
"ITEM_VALIDATION_FAILED",
"CATALOG_LIMIT_EXCEEDED"
"CATALOG_LIMIT_EXCEEDED",
"VAST_PARSE_FAILED",
"VAST_VERSION_MISMATCH",
"VAST_WRAPPER_DEPTH_EXCEEDED"
],
"enumDescriptions": {
"INVALID_REQUEST": "Request is malformed, missing required fields, or violates schema constraints. Recovery: correctable (check request parameters and fix).",
Expand Down Expand Up @@ -190,7 +193,10 @@
"FEED_FETCH_FAILED": "Platform could not fetch the catalog feed URL during sync_catalogs. Recovery: correctable (check URL accessibility, authentication, and that the content matches the declared feed_format).",
"INVALID_FEED_FORMAT": "Catalog feed content does not match the declared feed_format. Recovery: correctable (verify the feed content matches the declared format, e.g. XML for google_merchant_center).",
"ITEM_VALIDATION_FAILED": "One or more catalog items failed schema validation during sync_catalogs. Recovery: correctable (check item_issues for per-item rejection reasons and fix the offending items).",
"CATALOG_LIMIT_EXCEEDED": "The account has reached its maximum catalog count. Recovery: correctable (remove unused catalogs, or contact the seller to raise the limit)."
"CATALOG_LIMIT_EXCEEDED": "The account has reached its maximum catalog count. Recovery: correctable (remove unused catalogs, or contact the seller to raise the limit).",
"VAST_PARSE_FAILED": "A submitted `vast` asset failed document-level validation: the inline `content` (or the document fetched from `url`) is not well-formed XML, has no `<VAST>` root element, contains no `<Ad>` element, or an `<InLine>` linear creative carries no `<MediaFile>`. Returned by sellers that declare `creative_specs.vast_validation` of `document` or `wrapper`; sellers at the default `structural` level do not inspect the VAST document and MUST NOT return this code. Distinct from `VALIDATION_ERROR` (manifest-level format validation): the manifest was structurally valid, the VAST document inside it was not. Sellers SHOULD set `error.field` to the offending asset path and SHOULD populate `error.details.reason` with one of `not_xml`, `no_vast_root`, `no_ad`, `no_media_file`. Unresolved ad-server macros in URLs (`[MACRO]`, `${MACRO}`, `{UNIVERSAL_MACRO}`) are opaque tokens, not parse failures. Recovery: correctable (fix the VAST document and resubmit).",
"VAST_VERSION_MISMATCH": "A submitted `vast` asset's document declares a `<VAST version>` attribute that does not match the asset's declared `vast_version`, does not satisfy the format's `vast_version` requirement, or is not in the seller's declared `creative_specs.vast_versions`. Returned by sellers performing document-level VAST validation (`creative_specs.vast_validation` of `document` or `wrapper`). Distinct from `VERSION_UNSUPPORTED` (AdCP protocol version negotiation, unrelated to creative documents). Sellers SHOULD set `error.field` to the offending asset path and SHOULD include the accepted versions in `error.details.supported_versions`. Recovery: correctable (re-tag with an accepted VAST version, or correct the asset's declared `vast_version`).",
"VAST_WRAPPER_DEPTH_EXCEEDED": "Resolving a `vast` asset's wrapper chain failed: the chain exceeded the format's declared `max_wrapper_depth`, revisited a `VASTAdTagURI` already seen in the chain (a loop), or a hop did not resolve within the seller's per-hop timeout. Returned by sellers that declare `creative_specs.vast_validation: \"wrapper\"`. Sellers SHOULD set `error.field` to the offending asset path and SHOULD populate `error.details.reason` with one of `depth`, `loop`, `timeout`, plus `error.details.depth` with the depth reached. Recovery: correctable (flatten the chain, reduce redirects below the format's `max_wrapper_depth`, or fix the failing hop)."
},
"enumMetadata": {
"$comment": "Structured recovery classification and remediation hints for each error code. SDKs MUST consume this block instead of parsing 'Recovery: X' from enumDescriptions prose. Each entry is { recovery, suggestion }. recovery is one of: correctable (caller can fix and retry), transient (retry with backoff), terminal (no autonomous recovery - operator intervention required). enumDescriptions is retained for human readability and will continue to carry the canonical narrative; the recovery classification embedded in that prose is normative and MUST match the value here.",
Expand Down Expand Up @@ -561,6 +567,18 @@
"CATALOG_LIMIT_EXCEEDED": {
"recovery": "correctable",
"suggestion": "remove unused catalogs, or contact the seller to raise the limit"
},
"VAST_PARSE_FAILED": {
"recovery": "correctable",
"suggestion": "inspect error.details.reason (not_xml, no_vast_root, no_ad, no_media_file), fix the VAST document, and resubmit"
},
"VAST_VERSION_MISMATCH": {
"recovery": "correctable",
"suggestion": "re-tag with a version from error.details.supported_versions, or correct the asset's declared vast_version to match the document"
},
"VAST_WRAPPER_DEPTH_EXCEEDED": {
"recovery": "correctable",
"suggestion": "inspect error.details.reason (depth, loop, timeout); flatten the wrapper chain, reduce redirects below the format's max_wrapper_depth, or fix the failing hop"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@
"simid": {
"type": "boolean",
"description": "SIMID support for interactive video ads"
},
"vast_validation": {
"type": "string",
"enum": ["structural", "document", "wrapper"],
"default": "structural",
"description": "Level of VAST asset validation the seller performs at sync_creatives (including dry_run): 'structural' checks manifest shape and format requirements only and never inspects the VAST document; 'document' additionally parses the VAST document and can return VAST_PARSE_FAILED / VAST_VERSION_MISMATCH; 'wrapper' additionally resolves the wrapper chain and can return VAST_WRAPPER_DEPTH_EXCEEDED. Absent means 'structural'. See the VAST Validation section of the video channel documentation for the normative checks at each level."
}
}
},
Expand Down
Loading