Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt

Use this file to discover all available pages before exploring further.

Status: 3.1 release-candidate preparation. The spec is feature-complete for 3.1; the published @adcp/sdk and runtime grader are still on the latest beta while RC packaging is prepared. Pre-release adopters should pin an exact value advertised by the seller’s supported_versions list, such as adcp_version: "3.1-beta.7" today and "3.1-rc.1" once an RC artifact is published. Use "3.1" only after GA.
AdCP 3.1 is a minor release. Every 3.1 change is additive over 3.0: new fields are optional, no required field was removed, no shape changed in a way that breaks a 3.0-conformant client. No breaking changes. But schemas moved — 3.1 introduces meaningful new surfaces that solve production problems 3.0 left open. During beta/RC validation, use exact pre-release pins from supported_versions; move production pins to "3.1" at GA to pick up the new fields. This page is the curated adopter overview for the 3.1 minor release. Need the full 3.1 change list, per-PR detail, or migration tables? Use Release Notes § Version 3.1.0, the authoritative version record. For long-form normative reference, follow the link on each headline below.
Looking for the major v2 → v3 changes instead? See What’s New in AdCP 3. This page covers the 3.0 → 3.1 minor delta only.

Why upgrade

3.1 is the production-hardening release. 3.0 shipped the protocol surface — discovery, buy lifecycle, signals, creative library, brand identity. 3.1 closes the operational gaps that surfaced when real agents started running buys against real publishers:
  • You can debug your webhooks now. Buyer agents inspect their own recent delivery fires via webhook_activity[] on get_media_buys — HTTP status, fire time, idempotency_key — instead of guessing why their gateway returned 5xx.
  • You can see why a buy is impaired. When a creative gets pulled, an audience is suspended, a catalog item is withdrawn, or an event source goes quiet, the buy’s health flips to impaired and impairments[] lists every offline dependency with its package_ids and remediation hint. Both as a snapshot on get_media_buys and as a push fire via notification-type: impairment.
  • You can mirror wholesale product feed and wholesale signals feed without burning bandwidth. Conditional-fetch tokens (if_wholesale_feed_version — ETag-style), wholesale enumeration on signals (symmetric with products), and account-level wholesale feed webhooks let storefronts, federated marketplaces, and registries hold an up-to-date local replica of every connected agent’s buyable products and signals without re-fetching unchanged feed payloads on every poll. Two-layer cache model (cache_scope: "public" | "account") means most accounts dedupe into a single shared cache.
  • You can compose seller-offered signals on media products. Products can declare included_signals for bundled/planned signal metadata, signal_targeting_allowed for package-level signal selection, optional inline signal_targeting_options for product-specific menus and pricing, and signal_targeting_rules for include/exclude/grouping limits. Buyers apply selections through grouped targeting_overlay.signal_targeting_groups, while wholesale products can omit inline options and use get_signals as the selectable signal feed.
  • You can bind goals to vendor-attested measurement. Optimization goals can now reference (vendor, metric_id) pairs from real measurement vendors (DV, IAS, Adelaide, TVision, Lumen, Kantar, Upwave, Scope3, etc.) — not vendor-agnostic strings the seller can interpret however they want.
  • You can pin a release and stop fighting drift. Release-precision adcp_version ("3.1" at GA, exact pre-releases such as "3.1-beta.7" or "3.1-rc.1" during validation) on every request; sellers advertise their full supported_versions set and echo what they actually served. SDK constructor pins are now a real thing.
  • Sub-brands self-publish. A brand can publish its own canonical brand.json on its own domain while the corporate house declares ownership via a portfolio pointer — same reciprocal pattern as IAB’s ads.txt / sellers.json.
  • Creative formats have a canonical vocabulary. 12 canonical format_kind values + a publisher catalog discovery surface + a projection-ref mechanism. Creative agents also advertise buildable canonical outputs through creative.supported_formats[]; targetable entries SHOULD include stable capability_id values for build_creative routing.
  • Video inventory can declare placement semantics. Products and placements can declare OpenRTB-aligned video_placement_types so buyers can distinguish instream, accompanying-content, interstitial, and standalone video.
  • Verification badges are version-scoped. Public 3.1 badge issuance can run alongside active 3.0 badges; buyers pinned to a release read the matching badge version.
  • Action discovery is mechanical. Products advertise allowed_actions[]; media buys carry available_actions[]. Buyers pre-flight which mutations are valid instead of failing mid-flight.
  • Billing has finality. Row-level is_final + finalized_at on delivery; matching final + finalized_at + measurement_window on report_usage. Buyers know when the number stops moving and can reconcile invoices.
Plus a long tail of error-code clarity, auth tightening, idempotency rules, TMP IdentityMatch upgrades, and adagents.json scaling work — see the headline list below.

At a glance

Area3.03.1
brand.jsonInline brands[] under a single house documentDistributed: brands self-publish on their own domains; houses declare ownership via brand_refs[]; mutual-assertion trust; typed trademarks[]
Brand verificationbrand.json discovery onlyverify_brand_claim / verify_brand_claims — federated authoritative verification (partners can ask the brand if a claim belongs to it)
Dependency impactNo protocol surface for “a resource the buy depends on went offline”media_buy.health + impairments[] snapshot; notification-type: impairment webhooks; propagation_surfaces capability; impairment.coherence compliance invariant
Webhook foundationSpecced per-featureOne persistent-channel contract: snapshot/log duality, notification_id typed at envelope, per-account + per-resource subscription model
Webhook observabilityNo buyer-side delivery visibilitywebhook_activity[] on get_media_buys — buyers self-service debug their own missed fires
Wholesale feed mirroringRe-fetch wholesale on every poll to detect changesETag-style wholesale_feed_version / if_wholesale_feed_version conditional fetch on get_products / get_signals; cache_scope (public/account) on every response for two-layer cache layering
Wholesale signalsget_signals required signal_spec or deprecated signal_ids — no protocol-conformant way to enumerate the full priced signals feeddiscovery_mode: "wholesale" symmetric with get_products buying_mode: "wholesale"; paginated full wholesale signals feed enumeration with signal_ref identity and pricing_options[] populated
Signal identitySignalId / signal_id.source (catalog or agent) was the primary signal identity shapeSignalRef / signal_ref.scope is canonical: data_provider for provider-published adagents.json signals, signal_source for source-native signals, and product for product-local media-buy options. Legacy signal_id remains accepted during the migration window
Product signal metadatadata_provider_signals mixed legacy bundled metadata with no selectable package-level signal surfacedata_provider_signals is deprecated. Use included_signals for non-selectable bundled/planned signals and signal_targeting_options for selectable product-scoped signal options with pricing, activation handles, defaults, and grouping hints
Package signal targetingNo grouped buy-time surface for seller-offered named signals; storefronts overloaded audience fields or brief texttargeting_overlay.signal_targeting_groups: top-level operator: "all" with child any include groups and none exclusion groups. Product signal_targeting_rules declares selection mode, direct vs seller-planned resolution, and group limits
Wholesale feed webhooksNoneAccount-level webhooks registered via sync_accounts.accounts[].notification_configs[] carry product.* / signal.* / wholesale_feed.bulk_change change payloads with applies_to.scope; standard webhook signing and SSRF guards apply
Creative formatsFormat-by-name with per-publisher variants12 canonical format_kind values + publisher catalog discovery (adagents.json formats[]) + v1_format_ref for dual emission + size flexibility (fixed / multi-size / responsive); creative agents advertise buildable outputs in creative.supported_formats[] and SHOULD include capability_id on targetable entries
Version negotiationInteger adcp_major_version per requestRelease-precision adcp_version (e.g. "3.1" at GA or exact prereleases like "3.1-rc.1") + adcp.supported_versions advertisement + envelope echo. Integer field remains as backwards-compatible legacy
Optimization goalsevent + metric kinds, vendor-agnosticNew vendor_metric kind — bind goals to vendor-attested metrics; vendor_metric_optimization per-product capability; three-precondition rejection rule
Capability declarationsPer-protocol basicsNew: media_buy.buying_modes for wholesale products, signals.discovery_modes for wholesale signals, wholesale_feed_versioning, wholesale_feed_webhooks, supported_optimization_metrics, supported_target_kinds, media_buy.frequency_capping, media_buy.propagation_surfaces, creative.bills_through_adcp, capabilities.idempotency.in_flight_max_seconds
Video placement discoveryVideo products relied on free-text descriptions or placement namesvideo_placement_types on products, placements, and get_products.filters using AdCP-native names for OpenRTB video.plcmt values
Delivery reportingreach without window semantics; viewability is rate onlyreach_window (cumulative / period / rolling); viewability.viewed_seconds; windowed pull recovery via time_granularity + include_window_breakdown
Billing surfaceAuthority via billing_measurement; no finality markerRow-level is_final + finalized_at on delivery; final + finalized_at + measurement_window on report_usage; creative.bills_through_adcp capability + BILLING_OUT_OF_BAND error
Action discoveryNo structured action vocabulary on buys/productsallowed_actions[] on Product (advisory template); available_actions[] on get_media_buys / create_media_buy / update_media_buy responses
Auth + securitySingle AUTH_REQUIRED error; no transport-channel ruleAUTH_REQUIRED split into AUTH_MISSING (correctable) + AUTH_INVALID (terminal); CREDENTIAL_IN_ARGS rejects credentials in request payload; request-signing protocol_methods_* namespace
IdempotencyPer-call replay onlyRule 9 (concurrent retries) + Rule 10 (downstream reconciliation); IDEMPOTENCY_IN_FLIGHT error code; capabilities.idempotency.in_flight_max_seconds
Async envelopeTwo-shape submitted envelope for create-style tasksThree-shape envelope extended to sync_audiences
TMP IdentityMatchBasic request/responseserve_window_sec frequency-cap data flow; seller_agent_url required on request; optional package_ids
adagents.jsonAuthoritative-only discoveryManaged-network scale (20 MB cap + publisher_domains[] compact form); ads.txt managerdomain fallback; tightened revoked_publisher_domains[] semantics
Schema housekeepingx-adcp-hoist opt-in marker; allowed_values on text-asset-requirements; vast_tracker + daast_tracker asset types; optional currency/total_budget on create/update responses
Compliance suitePer-tool scenariosCapability-gated scenarios for frequency_cap_enforcement, per_creative_attribution, metric_mode, ROAS, audience_buy_flow, event_dedup_flow, performance_buy_flow, and product_signal_targeting; storyboard requires runtime gate; comply_test_controller sandbox gate; version-scoped badge evidence for 3.0 and 3.1

Headline features

Distributed brand.json — sub-brands self-publish

A brand can now publish its own canonical brand.json on its own domain while the corporate house declares ownership via a portfolio pointer (brand_refs[]). The hierarchy stays one level deep — only houses declare ownership. Trust resolves via mutual assertion: both sides reciprocate. Identity attributes (logos, colors, tone, tagline) trust on the leaf’s TLS alone; relationship trust (governance propagation, billable inclusion) gates on the reciprocal entry. Same shape as IAB’s ads.txt / sellers.json / app-ads.txt reciprocal-publication pattern, applied to brand identity. Plus: typed trademarks[] with optional status, license_type, licensor_domain, countries, nice_classes (cross-industry disambiguation). Compliance fields resolve strictest-of (brand-level can tighten, never weaken) while identity fields stay brand-wins. → Normative spec: brand.json § Distributed publishing · PR #4505

verify_brand_claim / verify_brand_claims — federated brand verification

Three new brand-protocol tasks let partners ask a brand authoritatively whether a claim belongs to it: a brand agent published at the brand’s own domain, queried by anyone who needs to verify trademark ownership, ad-creative claims, or asset rights. Federated by design — every brand agent answers for its own brand only. Reframes the email-based self-healing SHOULD from #4505 as a richer pull-based DRM-for-brand-identity surface. → Spec: Brand Protocol § verify_brand_claim · PRs #4540, #4603

Dependency-impact webhooks and snapshot coherence

When a resource a media buy depends on transitions to an offline state — an audience suspended, a creative rejected post-approval, a catalog item withdrawn, an event source quiet, a property depublished — buyers see it through two parallel surfaces:
  • Snapshot. media_buy.health flips from ok to impaired; media_buy.impairments[] lists every offline resource with its package_ids, transition, reason_code, and remediation hint. The next get_media_buys read shows current truth.
  • Log. notification-type: impairment webhooks fire with notification_id = impairment_id and the same payload shape, configured via push_notification_config.
Either path is complete; buyers reconcile via the snapshot when push and pull disagree. Sellers declare which surfaces they use via capabilities.media_buy.propagation_surfaces (["snapshot"], ["webhook"], ["snapshot", "webhook"], or ["out_of_band"]). The impairment.coherence compliance invariant grades the contract end-to-end (forward, inverse, and health-iff rules; relaxes on terminal-status buys). → Spec: Media Buy Lifecycle § Health & impairments · Snapshot and log contract · RFC #2853 · PRs #4588, #4601, #4677, #4685, #4690

Webhook foundation + buyer-side delivery visibility

3.1 codifies one persistent-channel contract for every push surface: snapshot is authoritative, push is at-least-once and unordered, dedupe via idempotency_key, correlate state via notification_id (now typed at the envelope level on mcp-webhook-payload.json), replay = re-read the snapshot. Future webhook RFCs reference the foundation instead of re-deriving it. The subscription model extends to per-account so creative-library-level events (creative state changes) fire even when no media buy directly references the creative. For production debugging, buyers can opt-in to webhook_activity[] on get_media_buys — recent fires for the buys they see, with HTTP status, fire time, and idempotency_key. No more black-box “the publisher fired but my gateway returned 5xx and I can’t see it.” Pure self-service: buyers debug their own integration without operator round-trips. → Spec: Snapshot and log contract · Webhooks § Persistent channel contract · RFC #4582 · PRs #4601, #4701, #4730

Wholesale feed mirroring — conditional fetch, wholesale signals, webhooks

Three companion proposals let consumers (storefronts, federated marketplaces, registries, agency brand stacks) maintain a near-real-time local mirror of every connected AdCP agent’s buyable wholesale product feed and wholesale signals feed without burning bandwidth on per-poll wholesale fetches. Independent and complementary — agents MAY adopt any subset; consumers fall back to wholesale polling against agents that don’t. Terminology: this section uses wholesale feed for seller-side products and signals from get_products / get_signals. It is distinct from sync_catalogs, which uploads buyer-provided campaign input feeds to a seller account.
  • Conditional fetch (if_wholesale_feed_version). Every get_products / get_signals response returns an opaque wholesale_feed_version token. Pass it back on the next call and the seller MAY short-circuit with unchanged: true — no product or signal payload, no per-page diff. ETag/HTTP semantics. Optional companion pricing_version for sellers that move rate cards independently of structural metadata; if_pricing_version requires if_wholesale_feed_version (schema-enforced via dependencies). Backward-compatible: pre-v3.1 agents that ignore the tokens just return full payloads.
  • Wholesale signals (discovery_mode: "wholesale"). Callers can omit signal_spec / signal_refs / deprecated signal_ids and enumerate a signals agent’s full priced signals feed, paginated. Symmetric with get_products buying_mode: "wholesale", closing the gap that previously forced storefronts and marketplaces into hacky probe queries to mirror the signals feed.
  • Wholesale feed webhooks. Account-level webhooks registered through sync_accounts.accounts[].notification_configs[] emit product.{created,updated,priced,removed}, signal.{created,updated,priced,removed}, and wholesale_feed.bulk_change notifications. Each webhook carries core/wholesale-feed-webhook.json: the actual changed product/signal payload or bulk-change summary, the post-change wholesale_feed_version, and applies_to.scope for cache invalidation. There is no polling event task; consumers repair missed or distrusted pushes through get_products / get_signals.
Cache layering is the load-bearing design choice. Every response declares cache_scope: "public" | "account" (schema-required — the safety property of the two-layer cache depends on it). When the request had no account, MUST be "public". When the request had account, the seller declares "public" (this account prices off the rate card — buyer dedupes with the unauthenticated view) or "account" (custom overrides — buyer caches under the account key). Most accounts at most sellers price off the public layer, so a consumer holding N account caches typically dedupes into one public cache + a small number of overlays. Events carry applies_to.scope (with optional account_ids[]) so consumers invalidate the right cache layer — public events cascade to all overlays; account events touch only the named overlays. Sellers MAY downgrade an account from "account" back to "public" by returning a public-scope response on a previously-account-scoped tuple, signalling “this account no longer has overrides; drop the overlay.” Security posture is honest. The advisory-payload framing makes explicit that re-verifying a feed event against get_products / get_signals defends against transport tampering only — a compromised agent operator re-confirms its own lie. Operator-compromise defense lives in the existing trust anchors that gate spend (signed create_media_buy response, adagents.json pinned signing keys for marketplace-signal provenance), with content-signing of feed events deferred to the 4.0 R-1 root-of-trust track. Treat events as cheap mirror invalidation, not as the basis for any decision that commits dollars or authority. Capability declarations: wholesale_feed_versioning (conditional fetch + pricing_version_separate + cache_scope_account), wholesale_feed_webhooks (webhook change payloads), media_buy.buying_modes and signals.discovery_modes (wholesale support). Agents that advertise product.* webhook events must also advertise wholesale get_products; agents that advertise signal.* events must also advertise wholesale get_signals; wholesale_feed.bulk_change must name only a feed family backed by one of those repair paths. JSON Schema for the webhook envelope at core/wholesale-feed-webhook.json, wrapping core/wholesale-feed-event.json (discriminated on event_type with 9 branches + appliesTo / removalReason $defs). → Spec: Wholesale feed webhooks · get_products § Wholesale feed versioning · get_products § Cache layering · get_signals § Wholesale signals feed · PRs #4761 (conditional fetch), #4762 (wholesale signals), #4763 (feed webhooks), #4767 (cluster implementation)

Product-scoped signal targeting — included vs selectable signals

3.1 adds a product-scoped signal targeting contract for seller-offered signals on media buys. This closes the gap between broad signal discovery and the actual package-level buy surface:
  • included_signals describes signals already bundled into, included in, or seller-planned into a product. These are descriptive product metadata, not buyer-selectable controls.
  • data_provider_signals is deprecated. It remains for compatibility as legacy bundled metadata, but new implementations use included_signals for non-selectable signals and signal_targeting_options for selectable ones.
  • signal_targeting_allowed tells buyers the product has a package-level signal_targeting_groups surface. The default is false.
  • signal_targeting_options is the inline selectable menu when the product needs product-specific pricing, activation handles, defaults/fixed selections, grouping hints, or a brief/refine-selected subset. Wholesale products can omit this field and use get_signals as the selectable feed.
  • signal_targeting_rules declares the product-specific composition contract: direct targeting vs seller-planned resolution, optional/required/fixed selection, min/max counts, and grouping limits. This belongs on the product because a single seller may route products through different ad servers or planning layers.
Buyers apply selected signals with packages[].targeting_overlay.signal_targeting_groups. The portable baseline is intentionally simple: top-level operator: "all" with child operator: "any" groups for includes and child operator: "none" groups for exclusions. For binary signals, the signal expression uses value: true; exclusion is represented by the parent none group, not by value: false. Signal identity is also normalized. New payloads use signal_ref:
  • scope: "data_provider" + data_provider_domain + signal_id for signals defined in a provider’s published adagents.json signals.
  • scope: "signal_source" + signal_source_url + signal_id for source-native signals not published in upstream adagents.json signals.
  • scope: "product" + signal_id for product-local options meaningful only inside the selected product/package context.
Legacy SignalId / signal_id.source remains accepted during the minor-version migration window, including on get_signals, audience selectors, legacy flat signal targeting, and wholesale signal events, but SignalRef is the canonical shape for new clients. Legacy flat targeting_overlay.signal_targeting remains schema-valid but deprecated; new package-level composition uses signal_targeting_groups. → Spec: Product discovery § Signal targeting · Targeting § signal_targeting_groups · get_signals · PR #5009

Canonical creative formats — live, 12 canonicals, backwards-compatible

Live in 3.1, additive over 3.0. Products carry format_options[]: a list of ProductFormatDeclaration entries with a format_kind discriminator from the canonical enum. 12 canonicals: image, html5, display_tag, video_hosted, video_vast, audio_hosted, audio_daast, image_carousel, native_in_feed, responsive_creative, sponsored_placement, agent_placement. The enum also includes custom as the escape hatch for adopter-defined shapes that do not fit the canonicals. Three canonicals (sponsored_placement, responsive_creative, agent_placement) plus custom are tagged experimental within the framework; the remaining canonicals are non-experimental. The promotion queue for new canonicals is tracked in #3666. Backwards compatibility. The v1 format_ids path still works. ProductFormatDeclaration carries an optional v1_format_ref: [{agent_url, id}] array so v2 declarations link to one or more v1 named formats — sellers can dual-emit during the migration window. SDKs treat the enum as open at parse time: unknown future canonicals don’t fail validation; SDKs may surface a local routing status such as declared_only, but that status is not a 3.1 wire field. Publisher catalogs. list_creative_formats(publisher_domain="…") returns the publisher’s authoritative format list by reading <publisher_domain>/.well-known/adagents.json formats[], falling back to the AAO community mirror, then to agent-derived. Response carries source: "publisher" | "aao_mirror" | "agent_derived" so buyers know which tier produced the list. Size flexibility. Display canonicals declare size in three modes: fixed (width+height), multi-size (sizes: [{w,h}] — mirrors OpenRTB banner.format[]), or responsive (min_width/max_width/min_height/max_height). Mutually exclusive. → Spec: Canonical formats · PRs #3307, #4770

Release-precision version negotiation — pin your release

Every request and response now carries adcp_version (release-precision: "3.1" at GA, exact pre-releases like "3.1-beta.7" or "3.1-rc.1" during validation); sellers advertise their full supported_versions set on get_adcp_capabilities and echo the release they actually served at the envelope root. SDKs pin via a constructor option (adcpVersion: "3.1" JS, adcp_version="3.1" Python, WithAdcpVersion("3.1") Go at GA; exact prerelease strings before GA) and emit both the new string and the integer adcp_major_version mirror for compatibility with sellers that only read the legacy field. The integer remains functional through all of 3.x — additive ship, no required changes for 3.0-conformant agents. VERSION_UNSUPPORTED is typed with error.data.supported_versions[] echoed so retry doesn’t require an out-of-band lookup. → Spec: Versioning § Version negotiation · PR #3493

Vendor-attested measurement — vendor_metric goals + per-product capabilities

Optimization goals now support a third kind: "vendor_metric" shape — bind goals to vendor-attested metrics like attention (DV, IAS, Adelaide, TVision, Lumen), panel-based brand lift (Kantar, Upwave, Cint), emissions (Scope3, Good-Loop), and retail-media partner metrics. Closes the gap where 3.0’s vendor-agnostic enum values like attention_seconds were meaningless without a vendor binding. Sellers declare per-product vendor_metric_optimization with supported_metrics[] (the (vendor, metric_id) pairs the bidding stack can steer toward). A three-precondition rejection rule on goal acceptance — discovery, capability, reporting coherence — ensures the goal is steerable AND reportable end-to-end. Plus seller-level supported_optimization_metrics and supported_target_kinds on conversion_tracking for capability-gated compliance scenarios. → Spec: Optimization goals § vendor_metric kind · PRs #4668, #4669, #4649

Delivery reporting — reach_window, viewed_seconds, windowed pulls

Three additive surfaces close reporting gaps. reach_window declares the measurement window for reach and frequency (cumulative / period / rolling) — buyers MUST NOT sum reach across rows without it. viewability.viewed_seconds reports average in-view duration per measurable impression, the reporting-side counterpart to the viewed_seconds optimization goal. Windowed pull recovery on get_media_buy_delivery accepts time_granularity + include_window_breakdown: true, returning windows[] slices shape-aligned with reporting_webhook payloads at the same granularity — a buyer who missed a webhook fire reconstructs identical data by polling. Capability-scoped via reporting_capabilities.windowed_pull_granularities; sellers can honestly declare asymmetric webhook-vs-pull frequencies. → Spec: Delivery metrics reference · PRs #4618, #4601

Billing surface — authority, finality, and out-of-band

Two complementary changes close the billing-grade reporting story. Authority + finality flags: get_media_buy_delivery responses now carry row-level is_final + finalized_at on media_buy_deliveries[*] and on each by_package[*] — buyers know when a number stops moving and is safe for invoice reconciliation. Symmetric on report_usage: each usage record carries final (default true), finalized_at, and measurement_window. bills_through_adcp + BILLING_OUT_OF_BAND: creative agents declare via capabilities.creative.bills_through_adcp whether they bill on-protocol or out-of-band (flat license, SaaS, bundled enterprise — CM360 is the canonical case). Buyers pre-filter; sellers in out-of-band mode reject report_usage calls with the new BILLING_OUT_OF_BAND error rather than silently accepting. → Spec: Billing measurement · report_usage · PRs #4735, #4561

Action discovery — allowed_actions and available_actions

Structured action vocabulary for buy lifecycle mutations. Products advertise allowed_actions[] as an advisory template (which mutations the product generally supports, with modes[] and allowed_statuses[]). Media buys carry available_actions[] on get_media_buys / create_media_buy / update_media_buy responses — the current set of valid mutations for this buy in its current state. Buyers pre-flight which mutations are valid instead of issuing a call and getting INVALID_STATE. Finer-grained values added to media-buy-valid-action enum; legacy coarse values retained through 3.x for backwards compat (removed in 4.0). → Spec: Media Buy Lifecycle § Action discovery · PR #4514

Auth + security tightening

Four complementary changes: AUTH_REQUIRED split into AUTH_MISSING (correctable — retry with credentials) and AUTH_INVALID (terminal — credentials presented and rejected; rotate or escalate; do NOT auto-retry). Recovery classifications now match operator reality. CREDENTIAL_IN_ARGS new error code: sellers MUST reject requests that smuggle buyer-principal credentials into the task payload instead of the transport authentication channel — closes a prompt-injection exfiltration surface. Request-signing protocol_methods_* namespace — RFC 9421 signing scope tightened to the AdCP method surface only. comply_test_controller sandbox gate — every controller call MUST carry account.sandbox: true and the seller MUST verify against its persisted account record, not trust the field. Defense-in-depth boundary between sandbox and production. → Spec: Error handling § Recovery Classification · PRs #3739, #4057, #4326, #4382/#4392

Idempotency — Rules 9 + 10 + IDEMPOTENCY_IN_FLIGHT

Two new rules close production-edge cases. Rule 9 (concurrent retries): when a buyer retries before the original call has produced a cached response, the seller MAY return IDEMPOTENCY_IN_FLIGHT (new error code) instead of blocking — useful when the first call invokes a slow downstream system (SSP, ad server, payment provider). Buyers MUST treat it as transient and MUST NOT mint a fresh idempotency_key. Rule 10 (downstream reconciliation): explicit guidance on how buyers reconcile when an IDEMPOTENCY_EXPIRED response arrives and they have evidence the original succeeded — perform a natural-key check (e.g., get_media_buys by context.internal_campaign_id) before generating a fresh key. capabilities.idempotency.in_flight_max_seconds new capability — seller declares how long an in-flight call can take so buyers tune retry pacing. → Spec: Calling an agent § Idempotency · PRs #4402, #4409

TMP IdentityMatch upgrades

Three additive changes: serve_window_sec new required field on responses (1–300 seconds) — router caches the eligibility decision for this many seconds before re-querying. Replaces the prior ttl_sec framing with frequency-cap-data-flow-aware semantics. seller_agent_url now required on requests so the router can route the decision back to the originating seller. package_ids moved from required to optional — routers can ask “is this user eligible at all?” without enumerating packages. → Spec: TMP IdentityMatch implementation · PRs #4070, #3687

adagents.json — managed-network scale, manager-domain fallback, revocation semantics

Three production-scale improvements. Managed-network scale: authoritative adagents.json now caps at 20 MB; managers publishing large agent networks switch to a compact publisher_domains[] form that lists owned domains without inlining every property. Manager-domain fallback: when a publisher’s authoritative adagents.json is absent, crawlers fall back to the managerdomain declared in ads.txt — closes the discovery gap for S3 / CloudFront-hosted publishers that can’t return a 404 directly. Revocation semantics: revoked_publisher_domains[] is now strictly time-bound — revocation is a published fact with a discoverable timestamp, not a silent removal. Tightens trust propagation across the managed network. → Spec: adagents.json reference · PRs #4504, #4173, #4536

Compliance suite — capability-gated scenarios

Capability-gated storyboard scenarios let sellers run only what they claim. New scenarios for frequency_cap_enforcement, per_creative_attribution, metric_mode + ROAS (using a contains: matcher), audience_buy_flow, event_dedup_flow, and performance_buy_flow (capability-gated CPA buys). Plus a new requires runtime gate on Storyboards that conditions execution on declared capabilities — no more all-or-nothing scenarios. The full set is enumerated on the Compliance catalog. Beta compliance update: money-moving seller specialisms now exercise baseline sync_governance registration before spend-committing flows: sales-guaranteed, sales-non-guaranteed, sales-broadcast-tv, sales-catalog-driven, sales-social, and the generative seller flow under creative-generative. This does not make every seller governance-aware; governance-aware-seller remains the opt-in claim for check_governance consultation and propagation. Existing beta sellers claiming these specialisms must implement sync_governance registration and reject payloads with more than one governance_agents entry to remain conformant in 3.1 grading. → Spec: Compliance catalog · PRs #4312, #4642, #4664, #4722, #4727, #4731

Final-spec clarifications (WG-review batch)

Ten normative tightenings landed as the spec settled into beta. Mostly low-risk — adopters who’d already inferred reasonable defaults will continue working — but worth knowing about because the grader will check them at GA.
  • PROPOSAL_NOT_FOUND error code (#4043). Completes the proposal-lifecycle error catalog (alongside PROPOSAL_EXPIRED and PROPOSAL_NOT_COMMITTED). Sellers MUST return it when a referenced proposal_id isn’t recognized — wrong tenant, evicted from cache, or never finalized. Recovery: correctable.
  • Forward-compatible error.code decoding (#4227). Receivers MUST treat error.code as an open enum — decode unknown codes without rejecting, classify recovery from error.recovery, default to transient when recovery is absent. Senders from 3.1 onward MUST populate error.recovery on every error. Unblocks additive-in-patch for error codes on future maintenance lines without breaking pinned-version receivers.
  • idempotency_key required on every AdCP task request (#4399). Closes a longstanding gap where the spec said dedupe via idempotency_key but didn’t require buyers to send one. Sellers MAY reject requests missing the key after 3.1 GA.
  • MCP tool wrappers MUST tolerate envelope fields (#4399). The protocol envelope (status, context_id, context, task_id, timestamp, replayed, adcp_error, governance_context, idempotency_key) on MCP requests now goes through the wrapper layer instead of being rejected as “unexpected fields.” Closes a wrapper-layer bug where adopters had to omit envelope fields to call MCP successfully.
  • MCP serialization normalization (#2911). Drops payload.required from the protocol-envelope schema; adds the context field at envelope level; clarifies the flat-sibling MCP wire shape (envelope and body fields at the root, no nested payload: key). Adopters who’d implemented the de-facto flat shape are unaffected.
  • Idempotency replay returns historical snapshot (#4371). When a buyer retries a stateful create call (e.g., create_media_buy) within the replay window, the seller MUST return the historical snapshot of state-tracking fields (status, confirmed_at, etc.) — not the current state. Otherwise an at-most-once retry mutates the response from underneath the buyer.
  • refine[] finalize-exclusivity + multi-finalize atomicity (#4107). get_products refine[] semantics tightened: when one entry uses action: "finalize", that entry MUST be the only one in the array — multi-finalize is rejected. Multi-finalize across multiple proposals goes through separate get_products calls. Atomicity guarantee: finalize either succeeds completely or leaves the proposal unchanged.
  • pending_creatives status disambiguation (#4196). The description now explicitly states buyer action is required — sellers awaiting creative sync MUST surface what’s missing (creative count, deadline) in the response message rather than just emitting the status enum value. Clarifies adopter-facing UX without changing the wire shape.
  • notices advisory channel on runner-output-contract (#4418). Storyboard runners surface non-failure advisories (e.g., “agent still advertises a deprecated specialism, but the storyboard passed”) through a structured notices[] field on the run output. Replaces ad-hoc skip.detail prose carrying advisory text — unparseable for graders and dashboards.
  • Governance body-level status renamed (#4897). check_governance response: statusverdict (enum unchanged: approved / denied / conditions). report_plan_outcome response: statusoutcome_state (enum unchanged: accepted / findings). get_plan_audit_logs entries cascade: entries[].statusentries[].verdict for consistency. Frees the top-level status key for the envelope task-status under MCP flat-on-the-wire serialization (#4876, #2911). Migration: rename the property in every emitter and consumer of these three response shapes; values do not change. Governance is an experimental surface per x-status, so this is a sanctioned 3.1 wire-shape adjustment ahead of GA.
  • Media-buy body-level status collision — additive-deprecate (#4895). create_media_buy and update_media_buy success responses gain a new top-level media_buy_status field. The legacy top-level status: MediaBuyStatus form is marked deprecated: true and removed in 3.2 (#4906); compliance storyboards already require the new field. Nested status on get-media-buys-response, get-media-buy-delivery-response, and core/media-buy.json are out of scope here and addressed in the 4.0 cascade (#4905). Full migration: Migration › media_buy_status.
→ Full batch shipped as one commit: PR #4796 (4c124545f1). See per-issue links for the full prose.

Misc schema additions

media_buy.frequency_capping capability declaration (#4670) — seller declares which frequency-cap surfaces it honors. x-adcp-hoist opt-in marker (#4630) — canonically-shared object schemas declare themselves as hoistable into shared types. allowed_values on text-asset-requirements (#4333) — closed-set text assets (CTA, etc.) declare the allowed values so buyers can constrain generation. vast_tracker + daast_tracker asset types (#3051) — video and audio tracker assets. Optional currency + total_budget on create_media_buy / update_media_buy success responses (#4417). Async envelope to sync_audiences (#4571) — three-shape submitted envelope (Success / Error / Submitted) extended from create-style tasks. → See Release Notes § Version 3.1.0 for the per-PR detail.

Adopter action

If you are…What you need to do
A 3.0-conformant production agentNothing required — 3.1 changes are additive and a 3.0 client keeps working. During pre-release validation, pin exact advertised values; at GA, bump your SDK pin to "3.1" to pick up the new fields and emit adcp_version for forward-compatibility with future minors.
A buyer running production campaignsBump your SDK; during validation pass an exact prerelease from supported_versions, and at GA pass adcpVersion: "3.1" on construction. Implement webhook_activity[] reads when you suspect a missed fire. Read media_buy.health + impairments[] on every get_media_buys poll. Implement the impairment.coherence invariant in your reconciliation pipeline.
A seller running production buysSurface media_buy.health + impairments[] whenever a referenced resource transitions offline. Declare capabilities.media_buy.propagation_surfaces honestly. Implement webhook_activity[] so buyers can debug their integration end-to-end without operator round-trips — buyer self-service is integration-friction reduction, not seller charity. Mark is_final on every delivery row so buyers know when to reconcile.
A beta seller claiming money-moving specialismsImplement sync_governance on the account surface, register one governance agent per account using the brand/operator account reference, and reject registrations with multiple governance_agents entries. check_governance calls are still only required when also claiming governance-aware-seller.
A sub-brand team that wants self-publish authorityStand up /.well-known/brand.json at your own domain as a Brand Canonical Document. Declare house_domain: "<parent-house>". Ask the parent house team to reciprocate via brand_refs[].
A consumer maintaining a wholesale product feed and wholesale signals feed mirror (storefront, federated marketplace, registry)Bootstrap via get_products buying_mode: "wholesale" and/or get_signals discovery_mode: "wholesale"; persist the returned wholesale_feed_version + cache_scope. On subsequent polls send if_wholesale_feed_version and skip the full payload when the seller responds unchanged: true. If the agent declares wholesale_feed_webhooks.supported, register change webhooks through sync_accounts.accounts[].notification_configs[]; apply the webhook payload to the mirror and track applies_to.scope to invalidate the right cache layer (public vs. account overlay). Handle wholesale_feed.bulk_change or missed pushes by re-reading get_products / get_signals.
A signals agentDeclare signals.discovery_modes: ["brief", "wholesale"] to expose your full priced signals feed for mirroring. Return cache_scope on every response (REQUIRED — schema-enforced). Pre-3.1 callers without discovery_mode continue to get brief-mode behavior.
A sales / signals agent serving wholesale feed mirrors at scaleDeclare wholesale_feed_webhooks.supported: true and support webhook registration through sync_accounts.accounts[].notification_configs[], with standard account-level webhook signing, endpoint proof-of-control before activation, SSRF guards, account/caller authorization checks, and the notification-config fan-out cap. Keep event_types[] consistent with declared repair reads: product.* requires wholesale get_products, signal.* requires wholesale get_signals. The webhook emitter MUST include the actual changed product/signal payload or bulk-change summary and apply the same per-caller scope filter as your wholesale task at event-emission time — multi-tenant agents that can’t reliably scope events per-principal MUST NOT declare the capability.
A measurement vendor (attention, brand lift, emissions, retail)Publish your measurement.metrics[] catalog at your AdCP agent. Sellers declaring vendor_metric_optimization per product can now bind optimization goals to your (vendor, metric_id) pairs.
A creative agentDeclare capabilities.creative.bills_through_adcp honestly. If you bill out of band, reject report_usage calls with BILLING_OUT_OF_BAND rather than silently accepting.
An SDK authorPin published_version to the published 3.1 artifact you bundle; emit adcp_version (release-precision string) plus adcp_major_version (integer mirror); normalize semver values to release-precision before wire emission ("3.1.0-beta.1""3.1-beta.1"); surface VERSION_UNSUPPORTED as a typed error rather than auto-downshifting.

Migration

Bottom line: no breaking changes; additive only; you should bump. All 3.1 changes are additive over 3.0. New fields are optional, no required field was removed, and no shape changed in a way that breaks a 3.0-conformant client. A buyer running against a 3.1 seller without upgrading their SDK keeps working — they just won’t see the new fields. A seller running 3.0 schemas against a 3.1 buyer keeps working — the buyer’s new fields are silently ignored. But the new surfaces solve real production problems, and the longer you stay on 3.0 the more you’re operating without the production-hardening 3.1 added: webhook delivery debug, dependency-impact observability, billing finality flags, action discovery, vendor-attested measurement, release-precision negotiation. Bump as soon as your SDK is ready. The one publisher-visible behavior change is on brand.json trademarks[]: free-text status / countries values now validate against typed enum / ISO 3166-1 alpha-2 — non-conforming values surface as schema errors. If your trademarks[] published unrestricted free text, normalize the values before the 3.1 GA cut. Validator obligation for 3.1 SDKs. The wholesale feed mirroring work tightens one shape within 3.1: cache_scope is schema-required on every get_products / get_signals response (the safety property of the two-layer cache depends on it — see Cache layering). Pre-3.1 sellers correctly omit the field and remain conformant to their declared version. SDKs that validate strictly against the 3.1 schema MUST select the validator based on the server-declared adcp_version (the same release-precision mechanism 3.1 ships in version negotiation): for responses with adcp_version starting 3.0, the 3.1 cache_scope-required constraint MUST be relaxed. This is a tightening within 3.1, not a 3.0 break — but SDKs that hardcode the 3.1 schema without version-pinned validation will reject correct 3.0 traffic. Version-pinned validation is the right pattern for every 3.x→3.(x+1) tightening; cache_scope is the first time it’s load-bearing. For the per-PR detail, see Release Notes § Version 3.1.0. For the version-negotiation cadence and the 3.1 → 3.2 → 4.0 timeline, see Versioning & Governance § Migration timeline.