From c8dcf59c0a097c0b1df1fc3e907b5f398449d7d1 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 23 Aug 2026 05:57:54 +0200 Subject: [PATCH 01/36] feat(creative): add creative revision identity --- .changeset/add-creative-revision-identity.md | 7 + docs/contributing/x-entity-annotation.md | 2 +- .../sales-agent-creative-capabilities.mdx | 1 + docs/creative/specification.mdx | 136 ++++++++++++ .../task-reference/get_creative_delivery.mdx | 64 +++++- .../task-reference/list_creatives.mdx | 7 + .../task-reference/preview_creative.mdx | 2 +- .../task-reference/sync_creatives.mdx | 66 ++++++ docs/protocol/get_adcp_capabilities.mdx | 2 + docs/snippets/compliance-error-codes.mdx | 10 + package.json | 2 +- scripts/error-code-drift-dispositions.json | 5 + .../source/core/creative-revision-id.json | 10 + .../schemas/source/core/creative-variant.json | 22 +- .../schemas/source/core/x-entity-types.json | 4 + .../creative-status-changed-webhook.json | 16 ++ .../get-creative-delivery-response.json | 19 +- .../creative/list-creatives-response.json | 15 ++ .../creative/preview-creative-request.json | 3 +- .../creative/preview-creative-response.json | 3 +- .../creative/sync-creatives-request.json | 68 +++++- .../creative/sync-creatives-response.json | 22 +- static/schemas/source/enums/error-code.json | 6 + .../creative-revision-content-mismatch.json | 19 ++ static/schemas/source/index.json | 4 + .../get-adcp-capabilities-response.json | 19 ++ tests/creative-revisions.test.cjs | 195 ++++++++++++++++++ 27 files changed, 705 insertions(+), 24 deletions(-) create mode 100644 .changeset/add-creative-revision-identity.md create mode 100644 static/schemas/source/core/creative-revision-id.json create mode 100644 static/schemas/source/error-details/creative-revision-content-mismatch.json create mode 100644 tests/creative-revisions.test.cjs diff --git a/.changeset/add-creative-revision-identity.md b/.changeset/add-creative-revision-identity.md new file mode 100644 index 0000000000..aa86763a0b --- /dev/null +++ b/.changeset/add-creative-revision-identity.md @@ -0,0 +1,7 @@ +--- +"adcontextprotocol": minor +--- + +Add buyer-authored immutable creative revision identity across sync, review, +library readback, and delivery attribution, plus agent-unique served variant +identity for unambiguous post-flight preview replay. diff --git a/docs/contributing/x-entity-annotation.md b/docs/contributing/x-entity-annotation.md index 9393e0164d..d11370bf91 100644 --- a/docs/contributing/x-entity-annotation.md +++ b/docs/contributing/x-entity-annotation.md @@ -106,7 +106,7 @@ High-level groupings (see the registry for full descriptions). *Categories below | Brand & rights | `advertiser_brand`, `rights_holder_brand`, `rights_grant` | | Account & party | `account`, `operator` | | Media buy | `media_buy`, `package`, `product`, `proposal`, `product_pricing_option`, `spot_airing` | -| Creative | `creative`, `creative_format` | +| Creative | `creative`, `creative_revision`, `creative_locale_variant`, `creative_format`, `build_variant`, `served_variant` | | Data & targeting | `audience`, `audience_evidence`, `audience_evidence_snapshot`, `signal`, `signal_activation_id`, `demographic_interval_id`, `event_source` | | Lists & catalogs | `collection_list`, `property_list`, `catalog`, `catalog_generation`, `catalog_item`, `property` | | Plans & governance | `media_plan`, `governance_plan`, `governance_registry_policy`, `governance_inline_policy`, `governance_check`, `governance_delivery_statement`, `governance_delivery_observation`, `governance_outcome`, `governance_adjustment`, `governance_adjustment_evidence`, `seller_adjustment`, `content_standards`, `task`, `attestation_credential` | diff --git a/docs/creative/sales-agent-creative-capabilities.mdx b/docs/creative/sales-agent-creative-capabilities.mdx index 8db1c5d7f2..285dab3519 100644 --- a/docs/creative/sales-agent-creative-capabilities.mdx +++ b/docs/creative/sales-agent-creative-capabilities.mdx @@ -68,6 +68,7 @@ The endpoint may expose both facts, but they remain independent: |---|---| | `media_buy.inline_creative_management: true` | Package-scoped creative bodies are accepted on create/update media-buy operations | | `creative.has_creative_library: true` | The agent exposes reusable library operations such as `sync_creatives` and `list_creatives` | +| `creative.supports_revisions: true` | The library accepts immutable buyer `revision_id` values and preserves them through sync, review, readback, and delivery. Requires `has_creative_library: true`; does not imply staging or rollback. | | `creative.supported_formats[]` | The endpoint exposes canonical build, validation, or preview operations | An endpoint may support any applicable combination. Inline management does not imply a reusable library; a library does not imply generation. diff --git a/docs/creative/specification.mdx b/docs/creative/specification.mdx index 84e26525a4..7db5439d29 100644 --- a/docs/creative/specification.mdx +++ b/docs/creative/specification.mdx @@ -298,6 +298,141 @@ the normal lifecycle webhook. Sellers may reject new or replacement localization, but MUST still allow explicit `localization: null` cleanup when it does not violate an active assignment. +### Creative revision identity + +Buyers may send an optional buyer-assigned `revision_id` on each +`sync_creatives.creatives[]` item. Agents that advertise +`creative.supports_revisions: true` commit to the revision guarantees below. A +`creative_id` identifies the durable logical creative; `revision_id` identifies +one immutable buyer-authored input state beneath it. Revision support is an +identity and correlation contract, not a revision-history API: it does not add +staging, activation, rollback, or enumeration of prior revisions. + +Without that capability, a receiver may ignore `revision_id` and the buyer MUST +NOT rely on immutability, echo, readback, review correlation, or delivery +attribution. This capability gates reliance, not whether the open 3.2 request +shape may carry the field. + +Revision identity is scoped to `(creative_id, revision_id)`. On first +acceptance, the seller binds that identity to the creative's canonical revision +content before any seller transcoding, URL rewriting, normalization, or +delivery-representation production. Reusing the same identity with the same +content is idempotently acceptable across requests. Reusing it with different +content makes that creative item fail with +`CREATIVE_REVISION_CONTENT_MISMATCH`; its prior library state remains unchanged +while other items in the best-effort batch may still process. The failed result +omits `revision_id`; typed error details carry the rejected tuple. + +Canonical revision content includes every buyer creative-input field, including +unknown future content fields, except `creative_id`, `revision_id`, and the +explicit operational/metadata fields `name`, `tags`, `status`, `weight`, +`placement_refs`, and `placement_ids`. Format identity, assets, +`component_assets`, inputs, localization, provenance, rights, and industry +identifiers are therefore revision-bearing. This fail-closed rule prevents a +new content field from silently escaping revision identity. The exclusions +apply only to those exact top-level properties on the sync item; a nested field +with the same name remains content. + +For comparison, first apply the existing sync mutation semantics to the prior +buyer-authored state. In particular, an allowed omission of `localization` +means preserve the prior localization topology before comparison; it does not +make an otherwise identical revision mismatch. `localization: null` is a +mutation sentinel: consume it by removing localization from the effective state +before comparison, so a later omission represents the same unlocalized state. +Then remove those exact top-level excluded fields and encode the effective +buyer-authored state with RFC 8785 JSON Canonicalization Scheme (JCS). Receivers +MUST reject duplicate JSON object member names before this comparison. Do not +materialize schema defaults or perform seller normalization. Object member +order is therefore irrelevant while array order, unknown fields, and omission +or explicit `null` remain significant where that field's sync semantics treat +them as content. URL strings and any supplied content digests are part of the +input; revision identity does not prove that mutable bytes later served from +the same URL stayed unchanged. Buyers needing byte-level identity use immutable +URLs or digest-bound asset forms where available. + +A request's +`idempotency_key` is checked first and protects one request replay window; +revision identity is then checked across requests. A supporting seller retains +each revision ID-to-fingerprint binding for the lifetime of the creative and, +after a soft purge, for the creative tombstone lifetime. It need not retain or +expose historical payloads. A hard purge removes the binding; reusing the same +creative ID after its prior lifecycle has been purged begins a new creative +incarnation. + +Accepted sync results echo the exact `revision_id`; `list_creatives` returns +the creative's current revision; `creative.status_changed` identifies the +revision reviewed; and every delivered execution derived from a revision +returns it in `get_creative_delivery.variants[].revision_id`. Seller-generated +transcodes, normalized tags, or alternate delivery representations remain +derived from that revision and never mint a buyer revision. + +Historical delivery rows identify the revision that actually served. They need +not equal the revision currently returned by `list_creatives`; a report window +can span the promotion or replacement of one revision by another. + +Submitting a different `revision_id` with content canonically identical to the +current effective content updates the current revision identity, returns +`action: "updated"`, and preserves the current review state because no content +changed. If review is in flight, the seller atomically rebinds that review to +the new current revision so its eventual result and webhook name the new ID. +Repeating the current revision and content may return `unchanged`; a +metadata-only mutation may still return `updated`. Re-submitting a previously +bound historical revision first must match its retained fingerprint; if its +content differs from current content, accepting it makes it current and sends +it through ordinary review. A different revision with different content +becomes current only if the ordinary update is accepted and must enter review. + +When an existing unversioned creative adopts revision identity, canonically +identical effective content is accepted as `updated`, binds the supplied +`revision_id`, and preserves review state. If content also changes, the seller +binds that revision only if the ordinary update is accepted, and the changed +content enters review. + +When a supporting seller receives an update without `revision_id`, omission +preserves the current revision only if effective canonical content is unchanged +(for example, a metadata-only update). If accepted effective content changes, +the seller applies its legacy update rules and clears current revision identity; +it MUST NOT mint a seller-owned revision. `list_creatives`, subsequent status +webhooks, and new delivery rows then omit `revision_id` until a buyer supplies +one. Prior approval does not transfer: an accepted content-bearing unversioned +update enters the ordinary review lifecycle, subject to the same active-update +and deadline rules. Historical fingerprint bindings and historical delivery +attribution remain intact. + +Approval never transfers across changed revision content. A revision-aware +changed-content update uses a new `revision_id` and enters the seller's existing review/update +lifecycle. Existing active-delivery and deadline rules still decide whether the +update is accepted, paused, or rejected; revision support does not create a +staged inactive revision beside the current one. + +`revision_id` is correlation identity, not a compare-and-swap token or ordering +counter. Buyers MUST serialize writes to one creative. Sellers apply ordinary +last-accepted-update ordering. Except for an identical-content review explicitly +rebound to a new revision as described above, if a review result for an older revision arrives +after a newer revision became current, that stale result MUST NOT mutate the +current revision or its status and MUST NOT emit a status webhook as though it +applied to the current revision. + +When an accepted buyer update moves an approved creative into processing or +review, that buyer-initiated transition is acknowledged by the +`sync_creatives` result and does not fire `creative.status_changed`. A later +seller/system review approval or rejection uses the existing webhook contract. +This minimal revision contract does not +keep the prior approved revision serving during review; that behavior belongs +to the separately deferred staged-activation RFC. + +`dry_run: true` evaluates the candidate against existing revision bindings and +may echo a simulated revision result, but it MUST NOT create or update a +fingerprint binding or tombstone, change the current revision, or change review +state. + +For ordinary `sync_creatives` items, the effective-state projection above is +the complete revision preimage. A composed protocol container that carries a +set of equivalent delivery representations must define its own deterministic +projection over the complete set; comparing only one selected representation +is not sufficient to establish revision identity. The representation-set work +uses this extension point rather than minting a revision per representation. + ### Universal macros AdCP defines universal macros for cross-platform tracking. Creative agents MUST support these macros in tracking URLs: @@ -338,6 +473,7 @@ archived ── buyer unarchives ──▶ approved (or pending_review when re-r - `processing` → `rejected`: automatic when processing fails (corrupt file, unsupported codec, constraint violation) - `pending_review` → `approved`: seller approves after content policy review - `pending_review` → `rejected`: seller rejects with `rejection_reason` +- `approved` → `processing` or `pending_review` (buyer-initiated revision update): accepted only under the seller's ordinary active-delivery and deadline rules; acknowledged on `sync_creatives`, never via `creative.status_changed`. Changed revision content cannot inherit the prior approval. - `approved` → `suspended`: seller detects a recoverable dependency/authorization loss, such as `identity_authorization_revoked`, `identity_authorization_expired`, or `source_private` for a `published_post` reference. Sellers MUST surface a corresponding `impairment` on affected active buys. - `suspended` → `approved`: seller observes that the dependency is restored and any required re-review passes. - `suspended` → `rejected`: seller determines that a previously recoverable dependency/authorization loss cannot be restored for this creative, or that replacement/resubmission is required. Example: a revoked identity/post authorization for a `published_post` reference cannot be reauthorized. Sellers MUST keep affected active buys impaired until the creative is replaced, reassigned, or the package/buy is otherwise remediated. diff --git a/docs/creative/task-reference/get_creative_delivery.mdx b/docs/creative/task-reference/get_creative_delivery.mdx index a4e581b1da..0d3034d195 100644 --- a/docs/creative/task-reference/get_creative_delivery.mdx +++ b/docs/creative/task-reference/get_creative_delivery.mdx @@ -47,7 +47,7 @@ At least one scoping filter (`media_buy_ids` or `creative_ids`) is required. | `media_buy_id` | Seller's media buy identifier (present when the request spanned multiple media buys) | | `format_kind` | Canonical format of this creative | | `totals` | Aggregate delivery metrics across all variants — see [Delivery metrics fields](#delivery-metrics-fields) below | -| `variant_count` | Total number of variants (may exceed `variants` array length when `max_variants` is used) | +| `variant_count` | Total number of agent-unique `variant_id` rows (may exceed `variants` array length when `max_variants` is used). | | `variants` | Array of variant-level delivery data (empty if creative has no variants yet) | ### Delivery metrics fields @@ -94,24 +94,57 @@ Fields available on both `creative.totals` and each `variant` entry. Commonly-us ### Variant Object -Each variant represents a specific execution: a fixed creative (Tier 1), an asset combination the platform selected (Tier 2), or a generated variant (Tier 3). For catalog-driven packages, each catalog item rendered as a distinct ad execution is a variant — the variant's manifest includes the catalog reference with the specific item rendered. +Each variant row represents a specific execution within one source revision: a fixed creative (Tier 1), an asset combination the platform selected (Tier 2), or a generated variant (Tier 3). For catalog-driven packages, each catalog item rendered as a distinct ad execution is a variant — the variant's manifest includes the catalog reference with the specific item rendered. | Field | Description | |-------|-------------| -| `variant_id` | Platform-assigned variant identifier | +| `variant_id` | Agent-assigned identifier that uniquely and immutably identifies this served execution on the agent. It is the unambiguous lookup key when the agent supports variant preview. | +| `revision_id` | Buyer-authored input revision from which this execution was derived. Historical rows may name an older revision than the creative currently returned by [`list_creatives`](/docs/creative/task-reference/list_creatives). | | `locale_variant_id` | Buyer-assigned localized variant that supplied the served assets. Required for delivered variants of a localized creative, including default fallback; omitted for unlocalized creatives. | | `manifest` | (Optional) The rendered canonical creative manifest—the actual output served, with `format_kind` and resolved assets. | | `generation_context` | (Optional, Tier 3) Input signals that triggered generation — e.g., page topic, conversation theme, query category. Platforms provide summarized/anonymized signals, not raw user input. When content context is managed through AdCP content standards, includes an `artifact` reference linking to the specific content artifact. Supports `ext` for vendor-specific context structures. | | `ext` | (Optional) Platform-specific data. Social platforms use this for engagement metrics (upvotes, comments, shares) that vary by platform. | | Standard metrics | All [Delivery metrics fields](#delivery-metrics-fields) — same shape as `creative.totals` | -`creative_id` and `variant_id` are separate namespaces. The canonical build-to-delivery join is `build_creative.variants[].build_variant_id` → promoted `creative_id` → delivery `creative_id`. `variant_id` remains the platform's served execution variant id. +`creative_id`, `revision_id`, and `variant_id` are separate namespaces. The +canonical build-to-delivery join is `build_creative.variants[].build_variant_id` +→ promoted `creative_id` → delivery `creative_id`. `revision_id` identifies the +immutable buyer-authored input state beneath that creative; `variant_id` remains +the agent's unique AdCP served-execution identity. Every execution derived from +a revision carries that revision even when the seller transcodes or chooses +another equivalent delivery representation. + +### Revision attribution + +Do not compare every row to the creative's current `list_creatives.revision_id`. +A reporting window may contain executions from several accepted revisions; each +row carries the source revision that actually produced that execution. + +Every row is revision-homogeneous: all of its metrics come from exactly one +source revision and, when localized, one locale. `variant_id` is agent-unique +and MUST NOT be reused for a different revision, locale, or rendered manifest; +this keeps [`preview_creative`](/docs/creative/task-reference/preview_creative) replay by `variant_id` unambiguous when variant +preview is supported. An adapter +whose native ad platform reuses an identifier maps each distinct execution to +a distinct AdCP `variant_id` and may retain the native identifier in `ext`. +`variant_count` counts distinct `variant_id` values before applying +`max_variants`. Tier 1 therefore has one row per revision and locale represented +in the requested period, not necessarily one row for the whole period. A seller +that cannot partition delivery this way cannot advertise +`creative.supports_revisions: true`. + +For localized delivery, `locale_variant_id` is checked against the source +revision's accepted localization topology, not only the current +`list_creatives` topology. ### Localized delivery attribution For a localized creative, every returned execution MUST include -`locale_variant_id` and that value MUST exist in the creative's authoritative -`list_creatives.localization.variants[]` set. This identifies the assets that +`locale_variant_id` and that value MUST exist in the accepted localization +topology of the row's source revision. When that revision is current, the value +therefore also exists in `list_creatives.localization.variants[]`; historical +rows are checked against buyer-retained sync state rather than a newer current +topology. This identifies the assets that actually served after strict RFC 4647 Lookup, an explicit locale fallback, or the default action and is required for every such path. When the selected product format carries `locale_policy`, this value MUST also identify a variant @@ -125,7 +158,10 @@ Derived metrics like `ctr`, `completion_rate`, `roas`, and `cost_per_click` are ### Tier 1: Standard Creatives -One creative maps 1:1 to one variant. The variant metrics match the creative totals. +Within one source revision and locale, a standard creative maps 1:1 to one +variant. A report spanning revision or locale boundaries contains distinct +variant rows; a row's metrics match the creative totals only when it is the +sole row in the reporting period. ```json { @@ -375,7 +411,7 @@ When a sales agent implements both protocols, both tasks are available on the sa When running campaigns across multiple sellers, call `get_creative_delivery` on each agent separately and correlate results: - **Join key**: Use `creative_id` (buyer-assigned) to correlate the same creative across agents. If you used `concept_id` during upload, filter by concept to group related creatives. -- **`variant_id` scope**: Variant IDs are unique within an agent and creative, not globally. Two agents may generate variants with the same `variant_id` value. Prefix with the agent URL when building aggregated dashboards. +- **Variant row scope**: `variant_id` is unique and replayable within an agent. Use the structured tuple `(agent_url, media_buy_id-or-null, creative_id, variant_id)` as the globally scoped row key; `revision_id` and `locale_variant_id` are immutable attribution fields, not extensions of variant identity. - **Timezone handling**: Each agent may report in its own timezone via `reporting_period.timezone`. Normalize to a common timezone before aggregating metrics. - **`max_variants` selection**: Agents choose which variants to return when `max_variants` limits the result set. Most agents prioritize by impression volume (most-served first). For representative sampling, make multiple calls with different time ranges rather than relying on a single large `max_variants` value. @@ -389,7 +425,7 @@ When aggregating delivery data from multiple agents into a unified view, follow 3. **Merge by `creative_id`**: Group results by `creative_id` across agents. Sum `totals` (impressions, spend, clicks). Do not average derived metrics like `ctr` — recompute them from the summed components. -4. **Prefix `variant_id`**: Create globally unique variant keys by combining `agent_url + variant_id` (e.g., `https://sales.pinnaclemedia-example.com/var_a1b2c3`). This prevents collisions when two agents assign the same variant ID independently. +4. **Build a variant-row key**: Serialize the structured tuple `agent_url + media_buy_id-or-null + creative_id + variant_id`. Use actual `null`, not string sentinels, so legitimate IDs cannot collide with absence. Distinct revisions and locales already have distinct agent-assigned `variant_id` values. 5. **Group by `concept_id`**: For campaign-level roll-ups, use `concept_id` to group related creatives across sizes and sellers. Pull the concept-to-creative mapping from [`list_creatives`](/docs/creative/task-reference/list_creatives) on each agent. @@ -418,7 +454,12 @@ for (const [i, result] of results.entries()) { for (const v of creative.variants || []) { merged[key].variants.push({ ...v, - variant_id: `${agents[i].url}/${v.variant_id}`, // globally unique + variant_row_id: JSON.stringify([ + agents[i].url, + creative.media_buy_id ?? result.media_buy_id ?? null, + creative.creative_id, + v.variant_id, + ]), }); } } @@ -437,7 +478,8 @@ Agents that support this task expose `get_creative_delivery` in their tool surfa { "supported_protocols": ["creative"], "creative": { - "has_creative_library": true + "has_creative_library": true, + "supports_revisions": true } } ``` diff --git a/docs/creative/task-reference/list_creatives.mdx b/docs/creative/task-reference/list_creatives.mdx index eca991ee7f..c8946ca014 100644 --- a/docs/creative/task-reference/list_creatives.mdx +++ b/docs/creative/task-reference/list_creatives.mdx @@ -177,6 +177,7 @@ The response provides creative data with optional enrichment: "creatives": [ { "creative_id": "ft_88201", + "revision_id": "rev_2026_08_23_01", "name": "Holiday Sale - Medium Rectangle", "format_kind": "image", "status": "approved", @@ -204,11 +205,17 @@ The response provides creative data with optional enrichment: } ``` +When the seller advertises `creative.supports_revisions: true`, `revision_id` +is the current buyer-authored input revision for that creative. It is exact +readback of the value accepted by [`sync_creatives`](/docs/creative/task-reference/sync_creatives), not a seller processing or +transcode version. Older creatives without revision identity may omit it. + ### Per-creative fields | Field | Type | Description | |-------|------|-------------| | `creative_id` | string | Unique creative identifier | +| `revision_id` | string | Current buyer-authored input revision when the current content is revision-bound. Omitted for legacy/unversioned current content. | | `name` | string | Human-readable name | | `format_kind` | string | Canonical format kind | | `format_option_ref` | object | Optional exact publisher/product option reference | diff --git a/docs/creative/task-reference/preview_creative.mdx b/docs/creative/task-reference/preview_creative.mdx index a1570e29f5..518b0b6d04 100644 --- a/docs/creative/task-reference/preview_creative.mdx +++ b/docs/creative/task-reference/preview_creative.mdx @@ -186,7 +186,7 @@ All modes use a single flat object with `request_type` as the discriminant. | `item_limit` | integer | No | Maximum catalog items to render per preview variant. Used in single mode. | | `template_id` | string | No | Specific template ID for custom format rendering. Used in single mode. | | `requests` | array | Batch | Array of 1-50 preview requests. Each item supplies either a canonical `creative_manifest` or library `creative_id`, and may also carry `target_capability_id`, inputs, quality, output format, item limit, and template ID. | -| `variant_id` | string | Variant | Platform-assigned variant identifier from [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery). | +| `variant_id` | string | Variant | Agent-assigned, agent-unique AdCP served-execution identifier from [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery). | | `creative_id` | string | Conditional | Creative-library identifier. Use instead of `creative_manifest` to preview a stored canonical creative; in batch mode it is set per item. It may also provide creative context in variant mode. | | `allow_async` | boolean | No | Opt in to a `status: "submitted"` response for slow rendering. Defaults to `false`; agents MUST stay synchronous or return a terminal error when absent or false. | | `push_notification_config` | object | No | Optional terminal completion/failure webhook when `allow_async` is true and the agent returns `submitted`. The task remains pollable without it; this field alone never causes async execution. | diff --git a/docs/creative/task-reference/sync_creatives.mdx b/docs/creative/task-reference/sync_creatives.mdx index 974bed0899..88fb32c894 100644 --- a/docs/creative/task-reference/sync_creatives.mdx +++ b/docs/creative/task-reference/sync_creatives.mdx @@ -152,6 +152,7 @@ Implementations that acknowledge a creative on the synchronous success branch bu | Field | Type | Required | Description | | ------------------- | ------------------- | ----------- | ------------------------------------------------------------------ | | `creative_id` | string | Yes | Unique identifier for this creative | +| `revision_id` | string | No | Buyer-assigned input-content revision scoped to `creative_id`. May be sent to any 3.2 peer; `creative.supports_revisions: true` gates reliance on its guarantees. | | `name` | string | Yes | Human-readable name | | `format_kind` | CanonicalFormatKind | Yes for 3.2 authoring | Portable canonical format path. Pair with `format_option_ref` when exact routing is required. | | `format_id` | FormatId | Deprecated | Named-format compatibility path for older 3.x peers; mutually exclusive with `format_kind`. | @@ -164,6 +165,70 @@ New 3.2 integrations use `format_kind` with `format_option_ref` when routing dep Before uploading, buyers MUST verify each creative manifest against the target product's canonical `format_options[]`. The manifest MUST include every asset slot that the selected format option declares as required, keyed by that slot's `asset_group_id`. +### Creative revisions + +Buyers may send `revision_id` to any 3.2 peer. When the agent advertises +`creative.supports_revisions: true`, buyers may rely on it to correlate one +immutable input state through sync, review, readback, and delivery. Without the +capability, the receiver may ignore the field and the buyer cannot rely on +revision guarantees. A supporting seller echoes it on accepted `created`, `updated`, +and `unchanged` items and omits it on `failed` and `deleted` items. + +The identity is `(creative_id, revision_id)`. Reusing that pair with identical +canonical revision content is safe even under a new `idempotency_key`. Reusing +it with changed content fails that creative item with +[`CREATIVE_REVISION_CONTENT_MISMATCH`](/docs/building/verification/compliance-catalog#error-code-creative-revision-content-mismatch) and leaves its prior state unchanged; other batch items may still process. Mint a new `revision_id` for intentional +content changes. Metadata-only changes to `name` or `tags`, and assignment +changes, do not require a new revision. The seller checks `idempotency_key` +conflicts first, then revision content. + +Canonical revision content is RFC 8785 JCS of the effective buyer-authored +creative state after applying existing sync mutation semantics, then removing +exactly the top-level `creative_id`, `revision_id`, `name`, `tags`, +`status`, `weight`, `placement_refs`, and `placement_ids` fields. Sellers do not +materialize defaults or normalize values before this comparison. Nested fields +with those names remain content. Duplicate JSON member names are rejected. +An allowed omitted `localization` preserves the prior topology before +comparison. `localization: null` removes localization from the effective state +before comparison, so a later allowed omission represents that same +unlocalized state. URL identity is the submitted URL string (plus +any submitted digest), so use immutable or digest-bound asset references when +byte-level identity matters. + +A different revision ID with content identical to the current canonical state +is accepted as `updated`, becomes current, and preserves the existing review +state. A seller atomically rebinds any in-flight review to that new +identical-content ID. A previously bound historical ID must still match its +retained fingerprint and, when its content differs from current, enters review +again if accepted. A different revision with changed content enters review. Sellers retain revision +fingerprint bindings for the creative's lifetime (and any soft-purge tombstone +window); a hard purge starts a new creative incarnation. Revision IDs are not +ordering counters—buyers serialize writes, and stale review results for older +revisions cannot change the current revision or status. + +When an existing unversioned creative first adopts revision identity, identical +effective content is accepted as `updated`, binds the supplied ID, and preserves +review state. If content also changes, the seller binds the ID only when the +ordinary update is accepted, and changed content enters review. + +An omitted `revision_id` preserves current revision identity only for +content-equivalent updates. If a supporting seller accepts changed content from +a legacy unversioned caller, it clears current revision identity and does not +mint one; later readback, webhooks, and delivery omit the field while historical +bindings remain retained. Prior approval does not transfer, and an accepted +content-bearing unversioned update enters the ordinary review lifecycle. + +With `dry_run: true`, the seller evaluates existing revision conflicts and may +echo the simulated revision, but it does not reserve an ID, change bindings or +tombstones, select a current revision, or alter review state. + +Revision support does not add staged activation or revision history. A newly +accepted content revision follows the ordinary creative update and review +lifecycle, and prior approval does not transfer to changed content. The +buyer-initiated transition is acknowledged on this response rather than a +`creative.status_changed` webhook; subsequent seller/system review transitions +use the ordinary webhook. The prior revision is not kept serving during review. + ### Native localization Discover support before writing. The agent's @@ -400,6 +465,7 @@ The final per-creative `creatives` array lands on the task completion artifact, - All request fields - `platform_id` - Platform's internal ID (when `action` is not `failed`) +- `revision_id` - Exact request revision on accepted `created`, `updated`, or `unchanged` items when supplied. Omitted on `failed` and `deleted`. - `action` - Lifecycle operation performed by this sync: `created`, `updated`, `unchanged`, `failed`, `deleted` - `status` - **Advisory** review-lifecycle state ([`CreativeStatus`](https://adcontextprotocol.org/schemas/v3/enums/creative-status.json)): `processing`, `pending_review`, `approved`, `suspended`, `rejected`, `archived`. A UI hint and polling-scheduling signal — **not** a spend-authorization gate. Orthogonal to `action` — `action` describes what the sync did, `status` describes where the creative is in the review lifecycle. Values come from `CreativeStatus` only, never from `CreativeAction` (never put `created`/`updated`/`failed` in `status`). Sellers with async review return `processing` or `pending_review`; sellers with synchronous review MAY return a terminal value (`approved`/`rejected`) or `suspended` when a recoverable dependency/authorization gate prevents serving. **Buyers MUST NOT gate downstream spend or package activation on `status: approved` from this response** — reconcile via [`list_creatives`](/docs/creative/task-reference/list_creatives) or a signed review webhook before committing spend. Authoritative state is always via `list_creatives`. **MUST be omitted** when `action` is `failed` or `deleted` — failed items have no meaningful review state (see `errors`); deleted items are gone from the library. The schema enforces the omission rule via a conditional constraint. - `localization` - Exact materialized source/target locale state. Required with top-level `status` on every accepted localized item, omitted on failed/deleted/unlocalized items. The top-level status is the single creative-wide review state. diff --git a/docs/protocol/get_adcp_capabilities.mdx b/docs/protocol/get_adcp_capabilities.mdx index 97cb2d6a3d..ddd910051c 100644 --- a/docs/protocol/get_adcp_capabilities.mdx +++ b/docs/protocol/get_adcp_capabilities.mdx @@ -948,6 +948,8 @@ Creative protocol capabilities. Only present if `creative` is in `supported_prot | Field | Type | Description | |-------|------|-------------| | `supports_compliance` | boolean | When `true`, this creative agent can process briefs with compliance requirements and validate them against its canonical supported-format declarations. | +| `has_creative_library` | boolean | When `true`, the agent exposes reusable [`sync_creatives`](/docs/creative/task-reference/sync_creatives) and `list_creatives` library operations. | +| `supports_revisions` | boolean | When `true`, the library accepts buyer-authored immutable `revision_id` values and preserves them through sync echo, current readback, review attribution, and revision-homogeneous delivery rows. Requires `has_creative_library: true`. Does not imply revision history, rollback, or staged activation. | | `supported_formats` | object[] | Canonical creative operation catalog. New 3.2 producers MUST emit a stable `capability_id`, a full canonical `format` declaration, and explicit `operations` (`build`, `validate`, `preview`). Exact publisher support carries `{publisher_domain, format_option_id}` inside `format`; generic capabilities declare a satisfiable canonical parameter envelope. For 3.x compatibility, consumers accept entries without `capability_id` and interpret absent `operations` as `build`; such entries can be matched by contract but not selected through a capability-ID route. Replaces [`list_creative_formats`](/docs/creative/task-reference/list_creative_formats) in 3.2. | | `preview` | object | Per-route [`preview_creative`](/docs/creative/task-reference/preview_creative) declaration. `routes[].capability_id` names `supported_formats[]` entries whose `operations` includes `preview`; `rendering_origin` reports `platform_native` or `agent_approximation` per route. This self-description is informational and never grants authority; only a matching publisher placement `preview_provider` delegation does. See [rendering authority](/docs/creative/canonical-formats#rendering-authority-and-fallback-order). | | `supports_transformers` | boolean | When `true`, this creative agent offers account-scoped transformers — the selectable units of build capability (voices, models, styles) discovered via [`list_transformers`](/docs/creative/task-reference/list_transformers) and selected with `transformer_id` (plus the typed `config` bag) on [`build_creative`](/docs/creative/task-reference/build_creative). When `false` or absent, the agent does not expose transformers; `list_transformers` is unavailable and `build_creative` ignores `transformer_id`/`config`. Pre-call discriminator for routing across creative agents. | diff --git a/docs/snippets/compliance-error-codes.mdx b/docs/snippets/compliance-error-codes.mdx index b24052e3bd..dd443f1dd1 100644 --- a/docs/snippets/compliance-error-codes.mdx +++ b/docs/snippets/compliance-error-codes.mdx @@ -49,6 +49,7 @@ description: "Canonical AdCP error codes with recovery classifications, remediat | `CREATIVE_LOCALE_NOT_ACCEPTED` | correctable | supply or assign a creative variant matching every in-scope format option's accepted_language_ranges, narrow placement scope, choose a compatible format option, or change an ineligible serve_default | | `CREATIVE_NOT_FOUND` | correctable | verify creative_id via list_creatives, or sync_creatives to register it | | `CREATIVE_REJECTED` | correctable | revise the creative according to the applicable advertising policy or validated accessibility criteria | +| `CREATIVE_REVISION_CONTENT_MISMATCH` | correctable | resend the exact content previously bound to this revision_id, or mint a new revision_id for changed content | | `CREATIVE_VALUE_NOT_ALLOWED` | correctable | pick a value from error.details.allowed_values (or re-fetch the format) and resubmit | | `CREDENTIAL_IN_ARGS` | terminal | do NOT auto-retry — auto-retry re-logs the credential on each attempt. Move authentication material or caller-supplied trust material out of request args (top-level, {"context"}, {"ext"}, any nested location) onto the relevant transport authentication/trust channel or account provisioning path (Authorization: Bearer, RFC 9421 signature/JWKS, mTLS, MCP/A2A authentication framing); rotate any leaked credential, then resubmit | | `EVALUATOR_AGENT_NOT_ACCEPTED` | correctable | replace the evaluator agent_url (evaluator.feature_agent.agent_url or the evaluator agent-form agent_url) with one from the seller's published accepted_verifiers, or drop the evaluator agent pointer to fall back to seller-default ranking | @@ -458,6 +459,15 @@ Referenced creative does not exist in the agent's creative library. Recovery: co Creative failed content-policy, brand-safety, or accessibility review. For deadline violations, see CREATIVE_DEADLINE_EXCEEDED. Accessibility failures SHOULD use structured details conforming to error-details/accessibility-violation.json. Recovery: correctable (revise the creative according to the applicable policy or validated accessibility criteria). + + + + + +**Suggested action:** resend the exact content previously bound to this revision_id, or mint a new revision_id for changed content + +A sync_creatives item reused a revision_id for different canonical revision content under the same creative_id. Revision identity is scoped to the parent creative and immutable after first acceptance. Sellers MUST evaluate the buyer input before transcoding or normalization, MUST leave the prior creative state unchanged, and SHOULD return details conforming to error-details/creative-revision-content-mismatch.json. Distinct from IDEMPOTENCY_CONFLICT: idempotency_key protects one request replay window, while revision identity protects creative content across requests and retention. Recovery: correctable (resend the exact content previously bound to this revision_id, or mint a new revision_id for the changed content). + diff --git a/package.json b/package.json index a23b8f61cd..fb5d2d5eee 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "deploy:cdn-artifacts-cutover:dry-run": "wrangler deploy --config workers/artifact-cdn/wrangler.cutover.toml --dry-run", "verify:cdn-artifacts-cutover": "node scripts/verify-cdn-artifacts-cutover.mjs", "typecheck": "tsc --project server/tsconfig.json --noEmit", - "test:schemas": "node tests/schema-validation.test.cjs && node --test tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting", + "test:schemas": "node tests/schema-validation.test.cjs && node --test tests/outcome-target.test.cjs tests/trusted-match-offer-creative-data.test.cjs tests/accessibility-violation-details.test.cjs tests/portfolio-routing-scope.test.cjs tests/catalog-item-availability-updates.test.cjs tests/compact-product-lifecycle-storyboards.test.cjs tests/timezone-resolution-storyboards.test.cjs tests/schema-deprecation-metadata.test.cjs tests/products-only-brief-compatibility.test.cjs tests/async-identity-convergence.test.cjs tests/creative-rotation.test.cjs tests/creative-revisions.test.cjs tests/lint-schema-enum-drift.test.cjs tests/synthetic-depiction.test.cjs tests/creative-rendering-authority.test.cjs && npm run test:premium-display-formats && npm run test:geo-region-targeting", "test:performance-feedback": "node --test --test-force-exit --test-timeout=30000 tests/performance-feedback-contract.test.cjs", "test:dist-schema-version-ids": "node --test --test-force-exit --test-timeout=30000 tests/dist-schema-version-ids.test.cjs", "test:examples": "node tests/example-validation-simple.test.cjs && npm run test:tmp-context-merge", diff --git a/scripts/error-code-drift-dispositions.json b/scripts/error-code-drift-dispositions.json index 6b27017652..9ef1fb2a06 100644 --- a/scripts/error-code-drift-dispositions.json +++ b/scripts/error-code-drift-dispositions.json @@ -51,6 +51,11 @@ "target_version": "3.1", "note": "Native creative-localization assignment rejection when a package's effective format locale_policy has no eligible materialized variant. New 3.1 creative surface; wire change — held for 3.1." }, + "CREATIVE_REVISION_CONTENT_MISMATCH": { + "disposition": "held-for-next-minor", + "target_version": "3.2", + "note": "Creative revision identity rejects reuse of one buyer revision_id for different input content under the same creative_id. Additive 3.2 creative-library vocabulary; held for 3.2." + }, "FEED_FETCH_FAILED": { "disposition": "held-for-next-minor", "target_version": "3.1", diff --git a/static/schemas/source/core/creative-revision-id.json b/static/schemas/source/core/creative-revision-id.json new file mode 100644 index 0000000000..4c6e71827f --- /dev/null +++ b/static/schemas/source/core/creative-revision-id.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/core/creative-revision-id.json", + "title": "Creative Revision ID", + "description": "Buyer-assigned identity for one immutable input-content state of a durable creative. Scoped to the parent creative_id. Seller transcoding, normalization, and delivery representations do not change this identity.", + "type": "string", + "minLength": 1, + "maxLength": 255, + "x-entity": "creative_revision" +} diff --git a/static/schemas/source/core/creative-variant.json b/static/schemas/source/core/creative-variant.json index 083eef58d0..9683a4f774 100644 --- a/static/schemas/source/core/creative-variant.json +++ b/static/schemas/source/core/creative-variant.json @@ -2,12 +2,13 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/core/creative-variant.json", "title": "Creative Variant", - "description": "A specific execution variant of a creative with delivery metrics. For catalog-driven packages, each catalog item rendered as a distinct ad execution is a variant — the variant's manifest includes the catalog reference with the specific item rendered. For asset group optimization, represents one combination of assets the platform selected. For generative creative, represents a platform-generated variant. For standard creatives, maps 1:1 with the creative itself.", + "description": "A revision- and locale-homogeneous execution-variant breakdown row with delivery metrics. For catalog-driven packages, each catalog item rendered as a distinct ad execution is a variant — the variant's manifest includes the catalog reference with the specific item rendered. For asset group optimization, represents one combination of assets the platform selected. For generative creative, represents a platform-generated variant. For standard creatives, maps 1:1 with the creative within one source revision and locale. A distinct source revision, locale, or rendered execution receives a distinct agent-unique AdCP variant_id, which is an unambiguous lookup key when the agent supports variant preview.", "x-adcp-validation": { "verifier_constraints": { "localized_parent": { "required_field": "locale_variant_id", - "member_of": "list_creatives.localization.variants[].locale_variant_id", + "member_of": "accepted_source_revision.localization.variants[].locale_variant_id", + "current_list_localization_not_required": true, "applies_to_default_fallback": true } }, @@ -22,7 +23,22 @@ "properties": { "variant_id": { "type": "string", - "description": "Platform-assigned identifier for this variant" + "description": "Agent-assigned identifier that uniquely and immutably identifies this served execution on the agent. It MUST NOT be reused for another source revision, locale, or rendered manifest. An adapter whose native platform reuses an identifier maps each distinct execution to a distinct AdCP variant_id and may retain the native identifier in ext. When the agent supports variant preview, this ID is its unambiguous lookup key.", + "x-entity": "served_variant" + }, + "revision_id": { + "$ref": "/schemas/core/creative-revision-id.json", + "description": "Buyer-authored input revision from which this served execution was derived. Required when that execution derives from a revision-aware creative. Historical reporting may legitimately return an older revision after list_creatives shows a newer current revision. Delivery optimization, generation, transcoding, or representation selection does not mint a revision.", + "x-adcp-validation": { + "verifier_constraints": { + "revision_attribution": { + "must_equal": "accepted_source_revision_for_this_execution", + "current_list_revision_not_required": true, + "row_metrics_revision_homogeneous": true + } + }, + "spec": "docs/creative/task-reference/get_creative_delivery.mdx#revision-attribution" + } }, "locale_variant_id": { "type": "string", diff --git a/static/schemas/source/core/x-entity-types.json b/static/schemas/source/core/x-entity-types.json index 3129b1fc34..df798a6a54 100644 --- a/static/schemas/source/core/x-entity-types.json +++ b/static/schemas/source/core/x-entity-types.json @@ -20,11 +20,13 @@ "product_pricing_option", "vendor_pricing_option", "creative", + "creative_revision", "creative_locale_variant", "creative_format", "transformer", "evaluator", "build_variant", + "served_variant", "audience", "audience_evidence", "audience_evidence_snapshot", @@ -75,11 +77,13 @@ "product_pricing_option": "A pricing tier on a seller's inventory product (CPM / CPC / CPCV / etc). `pricing_option_id` inside `core/package.json` and `media-buy/package-request.json`. Scoped to the seller's product rate card — not interchangeable with `vendor_pricing_option`.", "vendor_pricing_option": "A pricing tier offered by a vendor agent (rights agent, signals agent, creative agent, governance agent) for its own services. `pricing_option_id` via `core/vendor-pricing-option.json`, also surfaced in `brand/acquire-rights-*`, `signals/activate-signal-request`, `media-buy/build-creative-response`, and `creative/get-creative-features-response`. Scoped to the issuing agent; not interchangeable with `product_pricing_option`.", "creative": "A creative asset (library entry, buyer-assigned). `creative_id` across creative/*, brand/creative-approval-*, and media-buy/package-request.", + "creative_revision": "A buyer-assigned immutable input-content state beneath one durable creative. Identity is the tuple `(creative_id, revision_id)`. `revision_id` round-trips through sync_creatives, list_creatives, creative status webhooks, and delivery readback. Seller transcoding, normalization, and alternate delivery representations do not create a new revision.", "creative_locale_variant": "A buyer-assigned stable locale execution within one localized creative. `locale_variant_id` round-trips from core/creative-localization.json into sync_creatives and list_creatives readback, then attributes localized executions in get_creative_delivery. Scoped to the parent creative and deliberately distinct from build_variant (a build_creative output leaf) and variant_id (a provider execution observed in reporting).", "creative_format": "A format spec identified by the composite of `agent_url` + `id` (see core/format-id.json).", "transformer": "An account-scoped creative build capability offered by a creative agent (the creative analog of a product) — a voice, model, style, or director with typed config params and per-account pricing. `transformer_id` via `core/transformer.json`, discovered in `creative/list-transformers-response` and selected in `media-buy/build-creative-request`. Scoped to the issuing creative agent.", "evaluator": "An account-scoped house evaluator preset a buyer attaches to `build_creative` to rank best_of_n variants - the rank-side of the get_creative_features feature oracle. `evaluator_id` on `core/evaluator-spec.json`, selected in `media-buy/build-creative-request`. The evaluator_id itself is pre-provisioned/account-arranged; only the feature vocabulary it emits is discovered via get_adcp_capabilities governance.creative_features. Scoped to the issuing creative agent.", "build_variant": "A single produced creative variant leaf from build_creative — the leaf-level lineage anchor. `build_variant_id` on `media-buy/build-creative-response` BuildCreativeVariantSuccess `creatives[].variants[]`. Distinct from a served `variant_id` (delivery) and a `preview_id` (preview renders), and distinct from the call-level grouping `build_creative_id`. On the canonical promotion path, the kept build_variant_id becomes the durable creative_id; delivery joins then use creative_id.", + "served_variant": "An agent-assigned immutable creative execution observed in delivery reporting. `variant_id` is unique within the issuing agent and round-trips from get_creative_delivery into preview_creative variant replay when that capability is supported. A distinct source revision, locale, or rendered manifest receives a distinct AdCP variant_id even when the underlying ad platform reuses a native identifier. Distinct from build_variant, creative_revision, and creative_locale_variant.", "audience": "A buyer-managed audience (CRM, lookalike seed, suppression). `audience_id` in media-buy/sync-audiences-request.", "audience_evidence": "A provider-scoped logical series of population-level audience evidence. `evidence_id` in core/audience-evidence.json and core/audience-evidence-selection.json remains stable while immutable snapshots receive distinct snapshot ids and content digests.", "audience_evidence_snapshot": "An immutable seller-scoped audience-evidence snapshot. `snapshot_id` in core/audience-evidence.json and core/audience-evidence-selection.json MUST never be reused for different canonical evidence content.", diff --git a/static/schemas/source/creative/creative-status-changed-webhook.json b/static/schemas/source/creative/creative-status-changed-webhook.json index 80779971ea..fa01df7578 100644 --- a/static/schemas/source/creative/creative-status-changed-webhook.json +++ b/static/schemas/source/creative/creative-status-changed-webhook.json @@ -46,6 +46,22 @@ "description": "Seller's identifier for the creative whose status changed. References the same id space as `list_creatives` / `sync_creatives` responses.", "x-entity": "creative" }, + "revision_id": { + "$ref": "/schemas/core/creative-revision-id.json", + "description": "Buyer-authored input revision to which this transition applies. Required when the reviewed state has revision identity and the seller advertises creative.supports_revisions; it need not be the current list revision when the fire is received. Omitted for unversioned current content. A stale review outcome MUST NOT emit a fire as though it applied to a newer current revision.", + "x-adcp-validation": { + "verifier_constraints": { + "revision_transition_attribution": { + "required_capability": "creative.supports_revisions", + "required_when_transition_applies_to_bound_revision": true, + "must_equal": "revision_to_which_transition_applies", + "omit_for_unversioned_content": true, + "stale_result_must_not_target_current_revision": true + } + }, + "spec": "docs/creative/specification.mdx#lifecycle-webhooks" + } + }, "transition": { "type": "object", "description": "The status transition that triggered this fire. Valid `from` values are restricted to the prior states from which a seller/system-initiated transition can fire (`processing` for processing outcomes, `pending_review` for initial review outcomes, `approved` for re-review/revocation/seller-archive/recoverable suspension, `suspended` for seller-observed recovery or terminal escalation). The post-terminal states `rejected` and `archived` MUST NOT appear as `from` — those would require a buyer-initiated unblock (`sync_creatives` resubmit / unarchive), which does not fire this event.", diff --git a/static/schemas/source/creative/get-creative-delivery-response.json b/static/schemas/source/creative/get-creative-delivery-response.json index ce3bde9537..8698f10745 100644 --- a/static/schemas/source/creative/get-creative-delivery-response.json +++ b/static/schemas/source/creative/get-creative-delivery-response.json @@ -88,14 +88,29 @@ }, "variant_count": { "type": "integer", - "description": "Total number of variants for this creative. When max_variants was specified in the request, this may exceed the number of items in the variants array.", + "description": "Total number of agent-unique variant_id rows for this creative. When max_variants was specified in the request, this may exceed the number of items in the variants array.", "minimum": 0 }, "variants": { "type": "array", - "description": "Variant-level delivery breakdown. Each variant includes the rendered manifest and delivery metrics. For standard creatives, contains a single variant. For asset group optimization, one per combination. For generative creative, one per generated execution. Empty when a creative has no variants yet.", + "description": "Variant-level delivery breakdown. Each agent-unique variant_id identifies one immutable served execution and each row includes metrics from exactly one source revision and, for localized delivery, exactly one locale variant. A distinct revision, locale, or rendered manifest receives a distinct variant_id; metrics MUST NOT cross those boundaries. For standard creatives, contains one row per source revision and locale represented in the reporting period. For asset group optimization, one per combination, source revision, and locale. For generative creative, one per generated execution, source revision, and locale. Empty when a creative has no variants yet.", "items": { "$ref": "/schemas/core/creative-variant.json" + }, + "x-adcp-validation": { + "verifier_constraints": { + "revision_homogeneous_rows": { + "row_key": ["variant_id"], + "row_key_unique": true, + "variant_id_scope": "agent_unique_and_unambiguous_for_variant_preview_when_supported", + "native_identifier_reuse": "map_each_distinct_revision_locale_or_rendered_execution_to_a_distinct_adcp_variant_id", + "metrics_must_not_cross_revision_boundary": true, + "metrics_must_not_cross_locale_boundary": true, + "locale_variant_membership": "source_revision_localization_topology", + "variant_count": "count_distinct_row_keys_before_max_variants_limit" + } + }, + "spec": "docs/creative/task-reference/get_creative_delivery.mdx#revision-attribution" } } }, diff --git a/static/schemas/source/creative/list-creatives-response.json b/static/schemas/source/creative/list-creatives-response.json index 8cf24590d2..33a465c479 100644 --- a/static/schemas/source/creative/list-creatives-response.json +++ b/static/schemas/source/creative/list-creatives-response.json @@ -67,6 +67,21 @@ "description": "Unique identifier for the creative", "x-entity": "creative" }, + "revision_id": { + "$ref": "/schemas/core/creative-revision-id.json", + "description": "Current buyer-authored input revision for this creative. Present when the current effective content has revision identity and the seller advertises creative.supports_revisions; omitted after an accepted content-bearing legacy update without revision_id.", + "x-adcp-validation": { + "verifier_constraints": { + "revision_readback": { + "required_capability": "creative.supports_revisions", + "required_when_current_revision_is_bound": true, + "must_equal": "last_accepted_current_buyer_revision", + "omit_when_current_content_is_unversioned": true + } + }, + "spec": "docs/creative/task-reference/list_creatives.mdx#per-creative-fields" + } + }, "account": { "$ref": "/schemas/core/account.json", "description": "Account that owns this creative" diff --git a/static/schemas/source/creative/preview-creative-request.json b/static/schemas/source/creative/preview-creative-request.json index fa9d6dc888..f1ea69bdab 100644 --- a/static/schemas/source/creative/preview-creative-request.json +++ b/static/schemas/source/creative/preview-creative-request.json @@ -256,7 +256,8 @@ }, "variant_id": { "type": "string", - "description": "Platform-assigned variant identifier from get_creative_delivery response. Required when request_type is 'variant'." + "description": "Agent-assigned, agent-unique AdCP served-execution identifier from get_creative_delivery. Required when request_type is 'variant'.", + "x-entity": "served_variant" }, "creative_id": { "type": "string", diff --git a/static/schemas/source/creative/preview-creative-response.json b/static/schemas/source/creative/preview-creative-response.json index 98189895a1..a976597cc6 100644 --- a/static/schemas/source/creative/preview-creative-response.json +++ b/static/schemas/source/creative/preview-creative-response.json @@ -284,7 +284,8 @@ }, "variant_id": { "type": "string", - "description": "Platform-assigned variant identifier" + "description": "Agent-assigned, agent-unique AdCP served-execution identifier", + "x-entity": "served_variant" }, "creative_id": { "type": "string", diff --git a/static/schemas/source/creative/sync-creatives-request.json b/static/schemas/source/creative/sync-creatives-request.json index 0735a84aba..5762c5e47a 100644 --- a/static/schemas/source/creative/sync-creatives-request.json +++ b/static/schemas/source/creative/sync-creatives-request.json @@ -51,6 +51,10 @@ { "type": "object", "properties": { + "revision_id": { + "$ref": "/schemas/core/creative-revision-id.json", + "description": "Optional buyer-assigned identity for this exact input-content state. Buyers may send it to any 3.2 peer; creative.supports_revisions gates reliance on immutability, echo, readback, and delivery correlation, not whether the open request shape may carry the field. Within a supporting seller, a revision_id is immutable under one creative_id: reusing it with different canonical revision content MUST fail with CREATIVE_REVISION_CONTENT_MISMATCH. Metadata and assignment fields excluded from revision content may change without minting a new revision." + }, "localization": { "description": "Sync-only materialized-localization mutation. The top-level assets are the source variant; optional locale_fallbacks declare buyer-approved language-family substitutions, and default_locale_variant_id selects the final serving fallback. An object transactionally replaces the source assets and complete locale set, omission preserves existing localization only when the top-level source assets are unchanged, and null removes localization. This field never requests translation or generation. Receivers MUST advertise get_adcp_capabilities creative.localization before accepting it.", "anyOf": [ @@ -59,7 +63,63 @@ ] } }, - "additionalProperties": true + "additionalProperties": true, + "x-adcp-validation": { + "verifier_constraints": { + "revision_identity": { + "reliance_gate": "creative.supports_revisions", + "without_capability": "receiver_may_ignore_revision_id_and_buyer_must_not_rely_on_revision_guarantees", + "scope": ["creative_id", "revision_id"], + "check_order": ["idempotency_key", "revision_content"], + "canonical_content": { + "included": "all effective buyer-authored creative fields, including unknown future content fields", + "mutation_projection": "apply_existing_sync_field_semantics_to_prior_buyer_state_before_comparison", + "localization_omitted": "preserve_prior_localization_before_comparison_when_existing_localized_source_upsert_allows_omission", + "localization_null": "remove_localization_property_from_effective_state_before_comparison", + "canonicalization": "RFC_8785_JCS_of_effective_buyer_state_after_removing_exact_top_level_excluded_fields", + "defaults": "must_not_be_materialized_before_comparison", + "duplicate_json_members": "reject_request_before_comparison", + "excluded": [ + "creative_id", + "revision_id", + "name", + "tags", + "status", + "weight", + "placement_refs", + "placement_ids" + ], + "exclusion_scope": "top_level_creative_item_only", + "evaluated_before": "seller_transcoding_or_normalization" + }, + "same_revision_same_content": "unchanged_unless_metadata_changed", + "same_revision_different_content": "CREATIVE_REVISION_CONTENT_MISMATCH", + "first_revision_on_unversioned_creative": { + "same_effective_content": "accept_as_updated_bind_revision_and_preserve_review_state", + "different_effective_content": "bind_only_on_accepted_update_and_require_review" + }, + "different_revision_same_current_content": "accept_as_updated_make_current_preserve_review_state_and_rebind_inflight_review", + "previously_bound_revision_matching_historical_content": "accept_as_updated; require_review_if_content_differs_from_current", + "different_revision_different_content": "accept_as_updated_make_current_and_require_review", + "revision_omitted_with_current_revision": { + "same_effective_content": "preserve_current_revision_identity", + "different_effective_content": "accept_under_legacy_update_rules_and_clear_current_revision_identity", + "different_effective_content_review": "prior_approval_must_not_transfer_and_update_enters_ordinary_review", + "must_not_mint_seller_revision": true, + "historical_fingerprint_bindings": "retain" + }, + "content_change_review": "prior_approval_must_not_transfer", + "write_order": "last_accepted_sync_wins_callers_must_serialize_revision_writes", + "stale_review_result": { + "different_fingerprint_or_not_rebound": "must_not_mutate_current_revision_or_current_status", + "identical_content_rebound": "apply_to_current_revision_and_attribute_result_and_webhook_to_current_revision_id" + }, + "dry_run": "evaluate_against_existing_bindings_and_echo_simulated_result_but_do_not_create_or_update_bindings_tombstones_current_revision_or_review_state", + "fingerprint_retention": "retain_for_creative_lifetime_and_soft_purge_tombstone_lifetime; hard_purge_removes_binding" + } + }, + "spec": "docs/creative/task-reference/sync_creatives.mdx#creative-revisions" + } } ], "x-adcp-validation": { @@ -85,7 +145,11 @@ } }, "minItems": 1, - "maxItems": 100 + "maxItems": 100, + "x-adcp-validation": { + "unique_item_properties": ["creative_id"], + "spec": "docs/creative/task-reference/sync_creatives.mdx#request-parameters" + } }, "creative_ids": { "type": "array", diff --git a/static/schemas/source/creative/sync-creatives-response.json b/static/schemas/source/creative/sync-creatives-response.json index 3c21fcc29e..f577e5f38e 100644 --- a/static/schemas/source/creative/sync-creatives-response.json +++ b/static/schemas/source/creative/sync-creatives-response.json @@ -33,6 +33,10 @@ "description": "Creative ID from the request", "x-entity": "creative" }, + "revision_id": { + "$ref": "/schemas/core/creative-revision-id.json", + "description": "Exact revision_id from the matching request creative. Required for accepted created, updated, or unchanged items when the request supplied revision_id; forbidden on failed or deleted items." + }, "account": { "$ref": "/schemas/core/account.json", "description": "Account that owns this creative" @@ -148,7 +152,8 @@ "not": { "anyOf": [ { "required": ["status"] }, - { "required": ["localization"] } + { "required": ["localization"] }, + { "required": ["revision_id"] } ] } } @@ -160,6 +165,21 @@ "then": { "required": ["status"] } + }, + { + "x-adcp-validation": { + "verifier_constraints": { + "revision_echo": { + "request_match_key": "creative_id", + "request_field": "revision_id", + "accepted_actions": ["created", "updated", "unchanged"], + "required_when_request_field_present": true, + "must_equal_request": true, + "forbidden_actions": ["failed", "deleted"] + } + }, + "spec": "docs/creative/task-reference/sync_creatives.mdx#creative-revisions" + } } ] } diff --git a/static/schemas/source/enums/error-code.json b/static/schemas/source/enums/error-code.json index bbbe99b33a..88d4e4e177 100644 --- a/static/schemas/source/enums/error-code.json +++ b/static/schemas/source/enums/error-code.json @@ -21,6 +21,7 @@ "CREATIVE_REJECTED", "CREATIVE_LOCALE_NOT_ACCEPTED", "CREATIVE_VALUE_NOT_ALLOWED", + "CREATIVE_REVISION_CONTENT_MISMATCH", "UNSUPPORTED_FEATURE", "UNPRICEABLE_OUTPUT", "UNSUPPORTED_GRANULARITY", @@ -133,6 +134,7 @@ "CREATIVE_REJECTED": "Creative failed content-policy, brand-safety, or accessibility review. For deadline violations, see CREATIVE_DEADLINE_EXCEEDED. Accessibility failures SHOULD use structured details conforming to error-details/accessibility-violation.json. Recovery: correctable (revise the creative according to the applicable policy or validated accessibility criteria).", "CREATIVE_LOCALE_NOT_ACCEPTED": "A creative bound to a locale-constrained product format has no materialized variant matching locale_policy.accepted_language_ranges, lacks protocol-declared locale topology, or uses serve_default with a seller-ineligible default variant. Seller ranges use RFC 4647 Basic Filtering and are applied independently for every placement where the assignment may serve, before buyer Lookup, locale_fallbacks, or default selection. Distinct from CREATIVE_REJECTED because this is a mechanically discoverable assignment-eligibility mismatch, not subjective content review. error.field SHOULD point to the offending creative or assignment; error.details SHOULD include format_option_id when present, accepted_language_ranges, available_variant_locales, and placement identity when applicable. Recovery: correctable (supply or assign a matching materialized locale variant, narrow placement scope, choose a compatible format option, or change an ineligible serve_default).", "CREATIVE_VALUE_NOT_ALLOWED": "A submitted text-asset value is not in the format's declared `allowed_values` list. Distinct from `CREATIVE_REJECTED` (generic creative-review failure) by being a closed-set constraint violation that the buyer can resolve mechanically without policy interpretation — the seller has published the complete list of acceptable values on the format, and any value outside that list is rejected by definition. The seller MUST set `error.field` to the offending asset's path within the manifest (e.g., `creatives[0].creative_manifest.assets[0].value` or the field name declared by the format) and SHOULD include the format's `allowed_values` array in `error.details.allowed_values` so the buyer agent can re-prompt its LLM with constrained sampling. Recovery: correctable (select a value from `allowed_values` and resubmit).", + "CREATIVE_REVISION_CONTENT_MISMATCH": "A sync_creatives item reused a revision_id for different canonical revision content under the same creative_id. Revision identity is scoped to the parent creative and immutable after first acceptance. Sellers MUST evaluate the buyer input before transcoding or normalization, MUST leave the prior creative state unchanged, and SHOULD return details conforming to error-details/creative-revision-content-mismatch.json. Distinct from IDEMPOTENCY_CONFLICT: idempotency_key protects one request replay window, while revision identity protects creative content across requests and retention. Recovery: correctable (resend the exact content previously bound to this revision_id, or mint a new revision_id for the changed content).", "UNSUPPORTED_FEATURE": "A requested feature or field is not supported by this seller. When rejecting a `refine_proposals` request that uses a typed dimension omitted from an explicit `proposal_refinement.supported_dimensions` declaration, the error details SHOULD follow `error-details/unsupported-refinement-dimension.json` — `unsupported_dimension` names the offending dimension and `supported_dimensions` echoes the seller's declaration so the buyer can remove or translate the field without another capability round trip. Recovery: correctable (check get_adcp_capabilities and remove unsupported fields).", "UNPRICEABLE_OUTPUT": "A creative transformer build targets an output capability that no pricing option covers — no transformer.pricing_options entry has a matching applies_to_output_capability_ids and none is unscoped. The build is rejected rather than billed at a guessed rate. Recovery: correctable.", "UNSUPPORTED_GRANULARITY": "The requested `time_granularity` on `get_media_buy_delivery` is not in the product's declared `reporting_capabilities.windowed_pull_granularities`. Distinct from `UNSUPPORTED_FEATURE` (generic capability mismatch) by being narrowly about reporting-window granularity — the buyer asked for hourly pull-recovery on a product that only honors daily pulls, for example. Sellers MAY echo the declared set in `error.details.supported_granularities` when the caller is authorized to read the product's reporting capabilities — the same set is already available via `get_adcp_capabilities`, so the echo is a convenience, not load-bearing. Sellers MUST NOT echo a granularity set the caller could not otherwise read (per-product capability views vary by buyer entitlement). The `error.field` SHOULD point at `time_granularity`. Buyers that need higher-frequency recovery than the seller's pull set supports MUST rely on the webhook channel as primary at that frequency — the seller's `available_reporting_frequencies` may legitimately exceed `windowed_pull_granularities` (e.g., a stream-tap webhook on Kafka with warehouse pulls only at daily). Recovery: correctable (re-issue at a granularity in the declared set, or omit `time_granularity` to fall back to cumulative date-range pulls).", @@ -390,6 +392,10 @@ "recovery": "correctable", "suggestion": "verify the asset URLs in creative_manifest are reachable without agent-side credentials, then re-submit" }, + "CREATIVE_REVISION_CONTENT_MISMATCH": { + "recovery": "correctable", + "suggestion": "resend the exact content previously bound to this revision_id, or mint a new revision_id for changed content" + }, "INVALID_STATE": { "recovery": "correctable", "suggestion": "check current status via get_media_buys and adjust request" diff --git a/static/schemas/source/error-details/creative-revision-content-mismatch.json b/static/schemas/source/error-details/creative-revision-content-mismatch.json new file mode 100644 index 0000000000..bb9537ba83 --- /dev/null +++ b/static/schemas/source/error-details/creative-revision-content-mismatch.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/error-details/creative-revision-content-mismatch.json", + "title": "Creative Revision Content Mismatch Details", + "description": "Typed details for CREATIVE_REVISION_CONTENT_MISMATCH. Identifies the scoped revision without echoing creative content or seller-side fingerprints.", + "type": "object", + "properties": { + "creative_id": { + "type": "string", + "minLength": 1, + "x-entity": "creative" + }, + "revision_id": { + "$ref": "/schemas/core/creative-revision-id.json" + } + }, + "required": ["creative_id", "revision_id"], + "additionalProperties": false +} diff --git a/static/schemas/source/index.json b/static/schemas/source/index.json index 2aa01fb813..e5dbf78d8a 100644 --- a/static/schemas/source/index.json +++ b/static/schemas/source/index.json @@ -398,6 +398,10 @@ "$ref": "/schemas/core/creative-variant.json", "description": "A specific execution variant of a creative with performance metrics" }, + "creative-revision-id": { + "$ref": "/schemas/core/creative-revision-id.json", + "description": "Buyer-assigned immutable input-content revision identity scoped to a creative" + }, "property": { "$ref": "/schemas/core/property.json", "description": "An advertising property that can be validated via adagents.json" diff --git a/static/schemas/source/protocol/get-adcp-capabilities-response.json b/static/schemas/source/protocol/get-adcp-capabilities-response.json index 1fc4dd9c74..ae0d6643ae 100644 --- a/static/schemas/source/protocol/get-adcp-capabilities-response.json +++ b/static/schemas/source/protocol/get-adcp-capabilities-response.json @@ -1957,6 +1957,11 @@ "description": "When true, this agent hosts a creative library and supports list_creatives and creative_id references in build_creative. Creative agents with a library should also implement the accounts protocol (sync_accounts / list_accounts) so buyers can establish access.", "default": false }, + "supports_revisions": { + "type": "boolean", + "description": "When true, this agent accepts buyer-assigned revision_id on sync_creatives, enforces immutable revision content, echoes accepted revision identity, returns it from list_creatives, and attributes delivered executions to it. Revision support does not imply revision history, rollback, or staged activation.", + "default": false + }, "supports_generation": { "type": "boolean", "description": "When true, this agent can generate creatives from natural language briefs via build_creative. The buyer provides a message with creative direction, and the agent produces a manifest with generated assets. When false, build_creative only supports transformation or library retrieval.", @@ -2176,6 +2181,20 @@ "required": ["has_creative_library"] } }, + { + "if": { + "properties": { + "supports_revisions": { "const": true } + }, + "required": ["supports_revisions"] + }, + "then": { + "properties": { + "has_creative_library": { "type": "boolean", "const": true } + }, + "required": ["has_creative_library"] + } + }, { "if": { "properties": { diff --git a/tests/creative-revisions.test.cjs b/tests/creative-revisions.test.cjs new file mode 100644 index 0000000000..535b9b7f88 --- /dev/null +++ b/tests/creative-revisions.test.cjs @@ -0,0 +1,195 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); + +const SCHEMA_DIR = path.join(__dirname, '../static/schemas/source'); + +function schema(relativePath) { + return JSON.parse(fs.readFileSync(path.join(SCHEMA_DIR, relativePath), 'utf8')); +} + +async function compile(relativePath) { + const ajv = new Ajv({ + allErrors: true, + strict: false, + loadSchema: async uri => schema(uri.replace(/^\/schemas\//, '').split('#', 1)[0]) + }); + addFormats(ajv); + return ajv.compileAsync(schema(relativePath)); +} + +const revisionId = 'rev_2026_08_23_01'; + +test('sync request accepts scoped revision identity and publishes fail-closed content rules', async () => { + const validate = await compile('creative/sync-creatives-request.json'); + const request = { + idempotency_key: '550e8400-e29b-41d4-a716-446655440000', + account: { account_id: 'acct_acme' }, + creatives: [{ + creative_id: 'hero', + revision_id: revisionId, + name: 'Hero', + format_kind: 'image', + assets: { + image: { + asset_type: 'image', + url: 'https://cdn.example/hero.png', + width: 300, + height: 250 + } + } + }] + }; + assert.equal(validate(request), true, JSON.stringify(validate.errors)); + + const rules = schema('creative/sync-creatives-request.json') + .properties.creatives.items.allOf[1]['x-adcp-validation'].verifier_constraints.revision_identity; + assert.deepEqual(rules.scope, ['creative_id', 'revision_id']); + assert.equal(rules.reliance_gate, 'creative.supports_revisions'); + assert.deepEqual(rules.check_order, ['idempotency_key', 'revision_content']); + assert.equal(rules.canonical_content.included, 'all effective buyer-authored creative fields, including unknown future content fields'); + assert.equal( + rules.canonical_content.canonicalization, + 'RFC_8785_JCS_of_effective_buyer_state_after_removing_exact_top_level_excluded_fields' + ); + assert.equal(rules.canonical_content.exclusion_scope, 'top_level_creative_item_only'); + assert.equal(rules.canonical_content.duplicate_json_members, 'reject_request_before_comparison'); + assert.equal( + rules.canonical_content.localization_null, + 'remove_localization_property_from_effective_state_before_comparison' + ); + assert.equal(rules.same_revision_different_content, 'CREATIVE_REVISION_CONTENT_MISMATCH'); + assert.equal( + rules.first_revision_on_unversioned_creative.same_effective_content, + 'accept_as_updated_bind_revision_and_preserve_review_state' + ); + assert.equal( + rules.first_revision_on_unversioned_creative.different_effective_content, + 'bind_only_on_accepted_update_and_require_review' + ); + assert.equal( + rules.different_revision_same_current_content, + 'accept_as_updated_make_current_preserve_review_state_and_rebind_inflight_review' + ); + assert.equal( + rules.revision_omitted_with_current_revision.different_effective_content, + 'accept_under_legacy_update_rules_and_clear_current_revision_identity' + ); + assert.equal(rules.revision_omitted_with_current_revision.must_not_mint_seller_revision, true); + assert.equal( + rules.revision_omitted_with_current_revision.different_effective_content_review, + 'prior_approval_must_not_transfer_and_update_enters_ordinary_review' + ); + assert.equal( + rules.stale_review_result.different_fingerprint_or_not_rebound, + 'must_not_mutate_current_revision_or_current_status' + ); + assert.equal( + rules.stale_review_result.identical_content_rebound, + 'apply_to_current_revision_and_attribute_result_and_webhook_to_current_revision_id' + ); + assert.match(rules.dry_run, /do_not_create_or_update_bindings/); + assert.equal(rules.content_change_review, 'prior_approval_must_not_transfer'); + assert.deepEqual( + schema('creative/sync-creatives-request.json').properties.creatives['x-adcp-validation'].unique_item_properties, + ['creative_id'] + ); +}); + +test('sync response echoes accepted revisions and forbids them on failed or deleted items', async () => { + const validate = await compile('creative/sync-creatives-response.json'); + const accepted = { + status: 'completed', + creatives: [{ creative_id: 'hero', revision_id: revisionId, action: 'created' }] + }; + assert.equal(validate(accepted), true, JSON.stringify(validate.errors)); + + for (const action of ['failed', 'deleted']) { + const invalid = { + status: 'completed', + creatives: [{ creative_id: 'hero', revision_id: revisionId, action }] + }; + assert.equal(validate(invalid), false, `${action} unexpectedly retained revision_id`); + } + + const item = schema('creative/sync-creatives-response.json').oneOf[0].properties.creatives.items; + const echo = item.allOf.at(-1)['x-adcp-validation'].verifier_constraints.revision_echo; + assert.equal(echo.must_equal_request, true); + assert.deepEqual(echo.accepted_actions, ['created', 'updated', 'unchanged']); +}); + +test('revision capability requires a creative library', async () => { + const validate = await compile('protocol/get-adcp-capabilities-response.json'); + const base = { + status: 'completed', + adcp: { + major_versions: [3], + idempotency: { supported: true, replay_ttl_seconds: 86400 } + }, + supported_protocols: ['creative'], + creative: { has_creative_library: true, supports_revisions: true } + }; + assert.equal(validate(base), true, JSON.stringify(validate.errors)); + + for (const library of [false, undefined]) { + const invalid = structuredClone(base); + if (library === undefined) delete invalid.creative.has_creative_library; + else invalid.creative.has_creative_library = library; + assert.equal(validate(invalid), false, `supports_revisions accepted has_creative_library=${library}`); + } +}); + +test('revision identity reads back through library, webhook, and delivery schemas', async () => { + const registry = schema('core/x-entity-types.json'); + assert.ok(registry.enum.includes('creative_revision')); + assert.ok(registry.enum.includes('served_variant')); + + const listSchema = schema('creative/list-creatives-response.json'); + assert.equal( + listSchema.properties.creatives.items.properties.revision_id.$ref, + '/schemas/core/creative-revision-id.json' + ); + assert.equal( + listSchema.properties.creatives.items.properties.revision_id['x-adcp-validation'] + .verifier_constraints.revision_readback.must_equal, + 'last_accepted_current_buyer_revision' + ); + const webhookRevision = schema('creative/creative-status-changed-webhook.json').properties.revision_id; + assert.equal( + webhookRevision.$ref, + '/schemas/core/creative-revision-id.json' + ); + assert.equal( + webhookRevision['x-adcp-validation'].verifier_constraints.revision_transition_attribution.must_equal, + 'revision_to_which_transition_applies' + ); + + const validateVariant = await compile('core/creative-variant.json'); + assert.equal(validateVariant({ variant_id: 'served_1', revision_id: revisionId }), true, JSON.stringify(validateVariant.errors)); + assert.equal(schema('core/creative-variant.json').allOf[1].properties.variant_id['x-entity'], 'served_variant'); + assert.equal(schema('creative/preview-creative-request.json').properties.variant_id['x-entity'], 'served_variant'); + const attribution = schema('core/creative-variant.json').allOf[1].properties.revision_id['x-adcp-validation'] + .verifier_constraints.revision_attribution; + assert.equal(attribution.row_metrics_revision_homogeneous, true); + assert.equal(attribution.current_list_revision_not_required, true); + const rows = schema('creative/get-creative-delivery-response.json').properties.creatives.items + .properties.variants['x-adcp-validation'].verifier_constraints.revision_homogeneous_rows; + assert.deepEqual(rows.row_key, ['variant_id']); + assert.equal(rows.row_key_unique, true); + assert.equal(rows.variant_id_scope, 'agent_unique_and_unambiguous_for_variant_preview_when_supported'); + assert.equal(rows.metrics_must_not_cross_revision_boundary, true); + assert.equal(rows.metrics_must_not_cross_locale_boundary, true); +}); + +test('revision mismatch has a typed correctable error contract', async () => { + const validate = await compile('error-details/creative-revision-content-mismatch.json'); + assert.equal(validate({ creative_id: 'hero', revision_id: revisionId }), true, JSON.stringify(validate.errors)); + assert.equal(validate({ creative_id: 'hero' }), false); + + const errors = schema('enums/error-code.json'); + assert.ok(errors.enum.includes('CREATIVE_REVISION_CONTENT_MISMATCH')); + assert.equal(errors.enumMetadata.CREATIVE_REVISION_CONTENT_MISMATCH.recovery, 'correctable'); +}); From 6881e0bbada81ec47957b51591d7311f9174bcf3 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 23 Aug 2026 06:33:20 +0200 Subject: [PATCH 02/36] test(creative): align historical locale attribution --- tests/composed-schema-validation.test.cjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/composed-schema-validation.test.cjs b/tests/composed-schema-validation.test.cjs index 5c5dda5397..e512f59b2d 100644 --- a/tests/composed-schema-validation.test.cjs +++ b/tests/composed-schema-validation.test.cjs @@ -6917,18 +6917,19 @@ async function runTests() { await testSchemaValidation( '/schemas/core/creative-variant.json', { variant_id: 'served_4821', locale_variant_id: 'loc_es_es' }, - 'Delivery variants can attribute the localized assets that served' + 'Served variants can attribute the localized assets that served' ); testValidationAnnotation( '/schemas/core/creative-variant.json', { localized_parent: { required_field: 'locale_variant_id', - member_of: 'list_creatives.localization.variants[].locale_variant_id', + member_of: 'accepted_source_revision.localization.variants[].locale_variant_id', + current_list_localization_not_required: true, applies_to_default_fallback: true } }, - 'Delivery variants expose machine-readable localized attribution rules' + 'Served variants expose machine-readable historical localization attribution rules' ); const localizedCanonicalListItem = structuredClone(localizedListItem); From 82139fc7e59803edc16eac38f550928c090b6ba3 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 23 Aug 2026 06:42:59 +0200 Subject: [PATCH 03/36] fix(creative): gate served variant identity scope --- .../task-reference/get_creative_delivery.mdx | 20 ++++++++++--------- .../task-reference/preview_creative.mdx | 4 ++-- .../task-reference/sync_creatives.mdx | 4 ++++ .../schemas/source/core/creative-variant.json | 13 +++++++++--- .../get-creative-delivery-response.json | 5 ++++- .../creative/preview-creative-request.json | 2 +- .../creative/preview-creative-response.json | 2 +- tests/creative-revisions.test.cjs | 8 +++++++- 8 files changed, 40 insertions(+), 18 deletions(-) diff --git a/docs/creative/task-reference/get_creative_delivery.mdx b/docs/creative/task-reference/get_creative_delivery.mdx index 0d3034d195..9fe806c6ec 100644 --- a/docs/creative/task-reference/get_creative_delivery.mdx +++ b/docs/creative/task-reference/get_creative_delivery.mdx @@ -47,7 +47,7 @@ At least one scoping filter (`media_buy_ids` or `creative_ids`) is required. | `media_buy_id` | Seller's media buy identifier (present when the request spanned multiple media buys) | | `format_kind` | Canonical format of this creative | | `totals` | Aggregate delivery metrics across all variants — see [Delivery metrics fields](#delivery-metrics-fields) below | -| `variant_count` | Total number of agent-unique `variant_id` rows (may exceed `variants` array length when `max_variants` is used). | +| `variant_count` | Total number of distinct `variant_id` rows within this creative (may exceed `variants` array length when `max_variants` is used). | | `variants` | Array of variant-level delivery data (empty if creative has no variants yet) | ### Delivery metrics fields @@ -98,7 +98,7 @@ Each variant row represents a specific execution within one source revision: a f | Field | Description | |-------|-------------| -| `variant_id` | Agent-assigned identifier that uniquely and immutably identifies this served execution on the agent. It is the unambiguous lookup key when the agent supports variant preview. | +| `variant_id` | Agent-assigned served-execution identifier. Existing agents retain uniqueness within agent + creative. An agent advertising `creative.supports_revisions` uses an agent-unique immutable value, which is the unambiguous lookup key when it also supports variant preview. | | `revision_id` | Buyer-authored input revision from which this execution was derived. Historical rows may name an older revision than the creative currently returned by [`list_creatives`](/docs/creative/task-reference/list_creatives). | | `locale_variant_id` | Buyer-assigned localized variant that supplied the served assets. Required for delivered variants of a localized creative, including default fallback; omitted for unlocalized creatives. | | `manifest` | (Optional) The rendered canonical creative manifest—the actual output served, with `format_kind` and resolved assets. | @@ -109,8 +109,9 @@ Each variant row represents a specific execution within one source revision: a f `creative_id`, `revision_id`, and `variant_id` are separate namespaces. The canonical build-to-delivery join is `build_creative.variants[].build_variant_id` → promoted `creative_id` → delivery `creative_id`. `revision_id` identifies the -immutable buyer-authored input state beneath that creative; `variant_id` remains -the agent's unique AdCP served-execution identity. Every execution derived from +immutable buyer-authored input state beneath that creative; for a seller +advertising `creative.supports_revisions`, `variant_id` is an agent-unique AdCP +served-execution identity. Every execution derived from a revision carries that revision even when the seller transcodes or chooses another equivalent delivery representation. @@ -121,10 +122,11 @@ A reporting window may contain executions from several accepted revisions; each row carries the source revision that actually produced that execution. Every row is revision-homogeneous: all of its metrics come from exactly one -source revision and, when localized, one locale. `variant_id` is agent-unique -and MUST NOT be reused for a different revision, locale, or rendered manifest; -this keeps [`preview_creative`](/docs/creative/task-reference/preview_creative) replay by `variant_id` unambiguous when variant -preview is supported. An adapter +source revision and, when localized, one locale. For an agent advertising +`creative.supports_revisions`, `variant_id` is agent-unique and MUST NOT be +reused for another creative, revision, locale, or rendered manifest; this keeps +[`preview_creative`](/docs/creative/task-reference/preview_creative) replay by +`variant_id` unambiguous when variant preview is supported. An adapter whose native ad platform reuses an identifier maps each distinct execution to a distinct AdCP `variant_id` and may retain the native identifier in `ext`. `variant_count` counts distinct `variant_id` values before applying @@ -411,7 +413,7 @@ When a sales agent implements both protocols, both tasks are available on the sa When running campaigns across multiple sellers, call `get_creative_delivery` on each agent separately and correlate results: - **Join key**: Use `creative_id` (buyer-assigned) to correlate the same creative across agents. If you used `concept_id` during upload, filter by concept to group related creatives. -- **Variant row scope**: `variant_id` is unique and replayable within an agent. Use the structured tuple `(agent_url, media_buy_id-or-null, creative_id, variant_id)` as the globally scoped row key; `revision_id` and `locale_variant_id` are immutable attribution fields, not extensions of variant identity. +- **Variant row scope**: The published default scopes `variant_id` to agent + creative. An agent advertising `creative.supports_revisions` strengthens this to agent-unique and immutable. In either case, use the structured tuple `(agent_url, media_buy_id-or-null, creative_id, variant_id)` as the globally scoped reporting row key; `revision_id` and `locale_variant_id` are immutable attribution fields, not extensions of variant identity. - **Timezone handling**: Each agent may report in its own timezone via `reporting_period.timezone`. Normalize to a common timezone before aggregating metrics. - **`max_variants` selection**: Agents choose which variants to return when `max_variants` limits the result set. Most agents prioritize by impression volume (most-served first). For representative sampling, make multiple calls with different time ranges rather than relying on a single large `max_variants` value. diff --git a/docs/creative/task-reference/preview_creative.mdx b/docs/creative/task-reference/preview_creative.mdx index 518b0b6d04..a685fa5709 100644 --- a/docs/creative/task-reference/preview_creative.mdx +++ b/docs/creative/task-reference/preview_creative.mdx @@ -186,8 +186,8 @@ All modes use a single flat object with `request_type` as the discriminant. | `item_limit` | integer | No | Maximum catalog items to render per preview variant. Used in single mode. | | `template_id` | string | No | Specific template ID for custom format rendering. Used in single mode. | | `requests` | array | Batch | Array of 1-50 preview requests. Each item supplies either a canonical `creative_manifest` or library `creative_id`, and may also carry `target_capability_id`, inputs, quality, output format, item limit, and template ID. | -| `variant_id` | string | Variant | Agent-assigned, agent-unique AdCP served-execution identifier from [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery). | -| `creative_id` | string | Conditional | Creative-library identifier. Use instead of `creative_manifest` to preview a stored canonical creative; in batch mode it is set per item. It may also provide creative context in variant mode. | +| `variant_id` | string | Variant | Agent-assigned AdCP served-execution identifier from [`get_creative_delivery`](/docs/creative/task-reference/get_creative_delivery). It is agent-unique for agents advertising `creative.supports_revisions`; callers SHOULD also send `creative_id` for legacy agents whose IDs are scoped to agent + creative. | +| `creative_id` | string | Conditional | Creative-library identifier. Use instead of `creative_manifest` to preview a stored canonical creative; in batch mode it is set per item. In variant mode it disambiguates the legacy agent + creative identifier scope. | | `allow_async` | boolean | No | Opt in to a `status: "submitted"` response for slow rendering. Defaults to `false`; agents MUST stay synchronous or return a terminal error when absent or false. | | `push_notification_config` | object | No | Optional terminal completion/failure webhook when `allow_async` is true and the agent returns `submitted`. The task remains pollable without it; this field alone never causes async execution. | diff --git a/docs/creative/task-reference/sync_creatives.mdx b/docs/creative/task-reference/sync_creatives.mdx index 88fb32c894..ee60051133 100644 --- a/docs/creative/task-reference/sync_creatives.mdx +++ b/docs/creative/task-reference/sync_creatives.mdx @@ -147,6 +147,10 @@ Implementations that acknowledge a creative on the synchronous success branch bu | `validation_mode` | string | No | Validation strictness: `"strict"` (default) or `"lenient"` | | `delete_missing` | boolean | No | When true, creatives not in this sync are archived (default: false). Cannot be combined with `creative_ids`. Cannot delete creatives assigned to active, non-paused packages. | +Every `creatives[]` item in one request MUST have a unique `creative_id`. +Duplicate IDs make response correlation and write ordering ambiguous, so the +seller rejects the request rather than applying last-write-wins behavior. + ### Creative object | Field | Type | Required | Description | diff --git a/static/schemas/source/core/creative-variant.json b/static/schemas/source/core/creative-variant.json index 9683a4f774..7012e00c26 100644 --- a/static/schemas/source/core/creative-variant.json +++ b/static/schemas/source/core/creative-variant.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/core/creative-variant.json", "title": "Creative Variant", - "description": "A revision- and locale-homogeneous execution-variant breakdown row with delivery metrics. For catalog-driven packages, each catalog item rendered as a distinct ad execution is a variant — the variant's manifest includes the catalog reference with the specific item rendered. For asset group optimization, represents one combination of assets the platform selected. For generative creative, represents a platform-generated variant. For standard creatives, maps 1:1 with the creative within one source revision and locale. A distinct source revision, locale, or rendered execution receives a distinct agent-unique AdCP variant_id, which is an unambiguous lookup key when the agent supports variant preview.", + "description": "A revision- and locale-homogeneous execution-variant breakdown row with delivery metrics. For catalog-driven packages, each catalog item rendered as a distinct ad execution is a variant — the variant's manifest includes the catalog reference with the specific item rendered. For asset group optimization, represents one combination of assets the platform selected. For generative creative, represents a platform-generated variant. For standard creatives, maps 1:1 with the creative within one source revision and locale. Existing agents retain the published scope in which variant_id is unique within the agent and creative. An agent advertising creative.supports_revisions additionally mints a distinct agent-unique AdCP variant_id for every source revision, locale, or rendered execution so historical attribution and variant preview remain unambiguous.", "x-adcp-validation": { "verifier_constraints": { "localized_parent": { @@ -23,8 +23,15 @@ "properties": { "variant_id": { "type": "string", - "description": "Agent-assigned identifier that uniquely and immutably identifies this served execution on the agent. It MUST NOT be reused for another source revision, locale, or rendered manifest. An adapter whose native platform reuses an identifier maps each distinct execution to a distinct AdCP variant_id and may retain the native identifier in ext. When the agent supports variant preview, this ID is its unambiguous lookup key.", - "x-entity": "served_variant" + "description": "Agent-assigned served-execution identifier. The legacy contract scopes uniqueness to the agent and creative. When the agent advertises creative.supports_revisions, the identifier is agent-unique and MUST NOT be reused for another creative, source revision, locale, or rendered manifest. A revision-capable adapter whose native platform reuses an identifier maps each distinct execution to a distinct AdCP variant_id and may retain the native identifier in ext. When that agent supports variant preview, this ID is its unambiguous lookup key.", + "x-entity": "served_variant", + "x-adcp-validation": { + "verifier_constraints": { + "default_scope": "unique_within_agent_and_creative", + "when_creative_supports_revisions": "agent_unique_immutable_and_not_reused_across_creative_revision_locale_or_rendered_manifest" + }, + "spec": "docs/creative/task-reference/get_creative_delivery.mdx#revision-attribution" + } }, "revision_id": { "$ref": "/schemas/core/creative-revision-id.json", diff --git a/static/schemas/source/creative/get-creative-delivery-response.json b/static/schemas/source/creative/get-creative-delivery-response.json index 8698f10745..092d3b6e7a 100644 --- a/static/schemas/source/creative/get-creative-delivery-response.json +++ b/static/schemas/source/creative/get-creative-delivery-response.json @@ -102,7 +102,10 @@ "revision_homogeneous_rows": { "row_key": ["variant_id"], "row_key_unique": true, - "variant_id_scope": "agent_unique_and_unambiguous_for_variant_preview_when_supported", + "variant_id_scope": { + "default": "unique_within_agent_and_creative", + "when_creative_supports_revisions": "agent_unique_immutable_and_unambiguous_for_variant_preview_when_supported" + }, "native_identifier_reuse": "map_each_distinct_revision_locale_or_rendered_execution_to_a_distinct_adcp_variant_id", "metrics_must_not_cross_revision_boundary": true, "metrics_must_not_cross_locale_boundary": true, diff --git a/static/schemas/source/creative/preview-creative-request.json b/static/schemas/source/creative/preview-creative-request.json index f1ea69bdab..1a91068a82 100644 --- a/static/schemas/source/creative/preview-creative-request.json +++ b/static/schemas/source/creative/preview-creative-request.json @@ -256,7 +256,7 @@ }, "variant_id": { "type": "string", - "description": "Agent-assigned, agent-unique AdCP served-execution identifier from get_creative_delivery. Required when request_type is 'variant'.", + "description": "Agent-assigned AdCP served-execution identifier from get_creative_delivery. Required when request_type is 'variant'. It is agent-unique when the source agent advertises creative.supports_revisions; for legacy agents the published scope remains agent plus creative, and callers SHOULD also send creative_id to disambiguate reused values.", "x-entity": "served_variant" }, "creative_id": { diff --git a/static/schemas/source/creative/preview-creative-response.json b/static/schemas/source/creative/preview-creative-response.json index a976597cc6..5bada82d81 100644 --- a/static/schemas/source/creative/preview-creative-response.json +++ b/static/schemas/source/creative/preview-creative-response.json @@ -284,7 +284,7 @@ }, "variant_id": { "type": "string", - "description": "Agent-assigned, agent-unique AdCP served-execution identifier", + "description": "Agent-assigned AdCP served-execution identifier. Agent-unique for an agent advertising creative.supports_revisions; otherwise scoped by this response's creative_id.", "x-entity": "served_variant" }, "creative_id": { diff --git a/tests/creative-revisions.test.cjs b/tests/creative-revisions.test.cjs index 535b9b7f88..36c475e38e 100644 --- a/tests/creative-revisions.test.cjs +++ b/tests/creative-revisions.test.cjs @@ -179,7 +179,13 @@ test('revision identity reads back through library, webhook, and delivery schema .properties.variants['x-adcp-validation'].verifier_constraints.revision_homogeneous_rows; assert.deepEqual(rows.row_key, ['variant_id']); assert.equal(rows.row_key_unique, true); - assert.equal(rows.variant_id_scope, 'agent_unique_and_unambiguous_for_variant_preview_when_supported'); + assert.equal(rows.variant_id_scope.default, 'unique_within_agent_and_creative'); + assert.equal(rows.variant_id_scope.when_creative_supports_revisions, + 'agent_unique_immutable_and_unambiguous_for_variant_preview_when_supported'); + const variantIdentity = schema('core/creative-variant.json').allOf[1].properties.variant_id['x-adcp-validation'] + .verifier_constraints; + assert.equal(variantIdentity.default_scope, 'unique_within_agent_and_creative'); + assert.match(variantIdentity.when_creative_supports_revisions, /agent_unique/); assert.equal(rows.metrics_must_not_cross_revision_boundary, true); assert.equal(rows.metrics_must_not_cross_locale_boundary, true); }); From 51ae6b6c1d332f453c6e13b9e9d8412652893914 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 22 Aug 2026 10:20:25 +0200 Subject: [PATCH 04/36] feat(creative): define delivery and macro resolution contracts --- .changeset/creative-delivery-contracts.md | 5 + docs/contributing/x-entity-annotation.md | 2 +- docs/creative/asset-types.mdx | 22 +- docs/creative/canonical-formats.mdx | 2 +- docs/creative/channels/display.mdx | 78 ++- docs/creative/channels/video.mdx | 26 +- docs/creative/creative-manifests.mdx | 3 + docs/creative/delivery-variants.mdx | 235 ++++++++ .../creative/implementing-creative-agents.mdx | 4 +- docs/creative/specification.mdx | 2 +- .../task-reference/build_creative.mdx | 6 +- .../preview_creative-advanced.mdx | 2 +- docs/creative/universal-macros.mdx | 251 ++++++-- docs/protocol/get_adcp_capabilities.mdx | 29 + docs/snippets/compliance-error-codes.mdx | 26 +- package.json | 2 +- scripts/error-code-drift-dispositions.json | 10 + .../creative-delivery-resolution.json | 130 ++++ .../creative-delivery-resolution.schema.json | 61 ++ .../source/test-vectors/macro-processing.json | 309 ++++++++++ .../test-vectors/macro-processing.schema.json | 49 ++ .../source/core/assets/asset-union.json | 1 + .../source/core/assets/daast-asset.json | 17 +- .../core/assets/daast-tracker-asset.json | 16 +- .../source/core/assets/display-tag-asset.json | 132 +++++ .../source/core/assets/html-asset.json | 2 +- .../source/core/assets/javascript-asset.json | 2 +- .../core/assets/pixel-tracker-asset.json | 16 +- .../schemas/source/core/assets/url-asset.json | 20 +- .../source/core/assets/vast-asset.json | 19 +- .../core/assets/vast-tracker-asset.json | 16 +- .../schemas/source/core/creative-asset.json | 4 + .../core/creative-delivery-variant.json | 65 ++ .../source/core/creative-manifest.json | 4 + .../schemas/source/core/creative-source.json | 39 ++ .../core/delivery-variant-rejection.json | 26 + .../core/delivery-variant-selection.json | 23 + static/schemas/source/core/error.json | 32 + .../source/core/macro-bearing-url.json | 22 + .../source/core/macro-declaration.json | 238 ++++++++ .../schemas/source/core/macro-encoding.json | 33 ++ .../core/macro-resolution-capability.json | 131 ++++ .../source/core/macro-resolution-result.json | 157 +++++ .../source/core/macro-translation-target.json | 73 +++ .../requirements/url-asset-requirements.json | 2 +- .../requirements/vast-asset-requirements.json | 13 +- .../schemas/source/core/x-entity-types.json | 2 + .../creative/sync-creatives-response.json | 5 + .../creative/validate-input-result.json | 5 + static/schemas/source/enums/error-code.json | 20 +- .../schemas/source/enums/macro-dialect.json | 15 + .../source/enums/macro-mapping-status.json | 13 + .../enums/macro-processing-operation.json | 13 + .../source/enums/macro-resolution-reason.json | 21 + .../schemas/source/enums/macro-resolver.json | 15 + .../source/enums/macro-value-context.json | 8 + .../schemas/source/enums/url-asset-type.json | 3 +- .../source/formats/canonical/_base.json | 17 +- .../source/formats/canonical/display_tag.json | 17 +- .../source/formats/canonical/video_vast.json | 13 +- .../media-buy/build-creative-request.json | 18 +- .../get-adcp-capabilities-response.json | 41 +- tests/creative-delivery-contracts.test.cjs | 560 ++++++++++++++++++ 63 files changed, 3041 insertions(+), 102 deletions(-) create mode 100644 .changeset/creative-delivery-contracts.md create mode 100644 docs/creative/delivery-variants.mdx create mode 100644 static/compliance/source/test-vectors/creative-delivery-resolution.json create mode 100644 static/compliance/source/test-vectors/creative-delivery-resolution.schema.json create mode 100644 static/compliance/source/test-vectors/macro-processing.json create mode 100644 static/compliance/source/test-vectors/macro-processing.schema.json create mode 100644 static/schemas/source/core/assets/display-tag-asset.json create mode 100644 static/schemas/source/core/creative-delivery-variant.json create mode 100644 static/schemas/source/core/creative-source.json create mode 100644 static/schemas/source/core/delivery-variant-rejection.json create mode 100644 static/schemas/source/core/delivery-variant-selection.json create mode 100644 static/schemas/source/core/macro-bearing-url.json create mode 100644 static/schemas/source/core/macro-declaration.json create mode 100644 static/schemas/source/core/macro-encoding.json create mode 100644 static/schemas/source/core/macro-resolution-capability.json create mode 100644 static/schemas/source/core/macro-resolution-result.json create mode 100644 static/schemas/source/core/macro-translation-target.json create mode 100644 static/schemas/source/enums/macro-dialect.json create mode 100644 static/schemas/source/enums/macro-mapping-status.json create mode 100644 static/schemas/source/enums/macro-processing-operation.json create mode 100644 static/schemas/source/enums/macro-resolution-reason.json create mode 100644 static/schemas/source/enums/macro-resolver.json create mode 100644 static/schemas/source/enums/macro-value-context.json create mode 100644 tests/creative-delivery-contracts.test.cjs diff --git a/.changeset/creative-delivery-contracts.md b/.changeset/creative-delivery-contracts.md new file mode 100644 index 0000000000..6b85b235e8 --- /dev/null +++ b/.changeset/creative-delivery-contracts.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": minor +--- + +Add explicit creative delivery contracts for inline display tags, atomic paired redirects, and late-bound equivalent source representations. Define exact VAST asset versions versus product and seller acceptance sets, plus declared macro dialect, resolver ownership, encoding depth, capability matching, and per-token validation results. Add deterministic selection lineage, structured rejection errors, documentation, and conformance vectors for issues #6761–#6764. diff --git a/docs/contributing/x-entity-annotation.md b/docs/contributing/x-entity-annotation.md index d11370bf91..fee93a623b 100644 --- a/docs/contributing/x-entity-annotation.md +++ b/docs/contributing/x-entity-annotation.md @@ -106,7 +106,7 @@ High-level groupings (see the registry for full descriptions). *Categories below | Brand & rights | `advertiser_brand`, `rights_holder_brand`, `rights_grant` | | Account & party | `account`, `operator` | | Media buy | `media_buy`, `package`, `product`, `proposal`, `product_pricing_option`, `spot_airing` | -| Creative | `creative`, `creative_revision`, `creative_locale_variant`, `creative_format`, `build_variant`, `served_variant` | +| Creative | `creative`, `creative_revision`, `creative_representation`, `creative_locale_variant`, `creative_format`, `build_variant`, `served_variant` | | Data & targeting | `audience`, `audience_evidence`, `audience_evidence_snapshot`, `signal`, `signal_activation_id`, `demographic_interval_id`, `event_source` | | Lists & catalogs | `collection_list`, `property_list`, `catalog`, `catalog_generation`, `catalog_item`, `property` | | Plans & governance | `media_plan`, `governance_plan`, `governance_registry_policy`, `governance_inline_policy`, `governance_check`, `governance_delivery_statement`, `governance_delivery_observation`, `governance_outcome`, `governance_adjustment`, `governance_adjustment_evidence`, `seller_adjustment`, `content_standards`, `task`, `attestation_credential` | diff --git a/docs/creative/asset-types.mdx b/docs/creative/asset-types.mdx index 4745308c90..784615cb4c 100644 --- a/docs/creative/asset-types.mdx +++ b/docs/creative/asset-types.mdx @@ -175,6 +175,7 @@ Senders **SHOULD** include `url_type` on every URL asset. The valid values are: | Value | Mechanism | |---|---| | `clickthrough` | User-click destination (landing page or ad-tech redirector) | +| `ad_request` | Invokes a third-party display creative endpoint at impression time | | `tracker_pixel` | Fires HTTP GET, expects 1×1 pixel or 204 response (impression / event / 3P trackers) | | `tracker_script` | Loads as a `