Migrating from 3.2 to 3.3
3.3 is a minor release over 3.2. It adds optional fields, enum values, and capability declarations. No stable field, enum value, or task is removed; the one removal is the experimentalbuild_creative.evaluator.evaluator_id source form (see below). seller_id on event source and audience results is deprecated in favor of the new platform_id field. Natural-key account references after provisioning, the account identity-rekey machinery, and condition (b) of request_signature_required are also deprecated (see Deprecations). A caller opts into 3.3 only after the peer advertises the exact release in supported_versions. Existing 3.2 integrations that need nothing else can stay pinned to "3.2".
Read these first:
- Required to claim 3.3 behavior: rules that narrow what a conformant implementation may do, including unsupported webhook HMAC and batch bodies, responses that conform to the served release, refinement that carries compliance forward, bounded counterparty-asserted domain lists, seller revisions that advance on every committed change, flight start for committed buys only, flat totals for multi-qualifier metrics, enforceable domain identifiers, property list references through AdCP tasks, and dual emission of platform IDs,
request_accountas the single account write, the protocol envelope on every task response, major-only requests served the oldest release, closed schemas that stay closed, and capabilities bounded by structure. - Brand JSON size cap rises to 2 MiB, audience match counts, and the 4 MiB response-size default: three loosened guarantees, so a 3.2 peer can reject or misread what a 3.3 peer sends.
- Recommended: bound agent response size: a new SHOULD that tightens what an adopting buyer accepts.
- Send only to 3.3 peers: new fields a 3.2 validator rejects.
- Deprecations:
seller_idon event source and audience results,principal_kindandexpected_principal_kindon the principal tasks, the natural-key account reference after provisioning, the account identity-rekey machinery (identity_updates,operator_identity,ACCOUNT_MOVED,ACCOUNT_IDENTITY_CONFLICT,stable_account_id), and condition (b) ofrequest_signature_required; removal no earlier than 4.0. - SDK and code-generator maintainers: schema-shape changes that alter generated types.
Upgrade checklist
Withdrawn from 3.3, planned for 4.0
Three features are not part of 3.3. All three add terms tocommercial_terms, and commercial-terms stays closed within 3.x. A buyer must not be bound by terms it cannot read, and terms_digest signs exactly those terms. If the object were open, a 3.2 buyer could accept a 3.3 term it silently ignored, and a minor release never changes a closed schema (DR-0036). 4.0 gives commercial_terms a declared extension point with must-understand, and the three features come back through it. The decision is in adcp#8501.
Definition pins (published in beta.0 and beta.1, removed after beta.1)
3.3.0-beta.0 published seller-authored commercial_terms.definition_pins[] (#7960), required it from sellers serving callers that negotiated 3.3, and 3.3.0-beta.1 carried it forward. Both betas are immutable, so a seller or SDK built from either bundle has these surfaces to remove:
commercial_terms.definition_pins[], withcore/definition-pin.jsonandenums/definition-pin-ref-kind.json.- The
definition_changedvalue ofproposal-refinement-reasonand thedrifted_pins[]field onrefine_proposalsresult rows. - The
REFERENCE_DEFINITION_CHANGEDerror code anderror-details/reference-definition-changed.json. - The
definition_pin_probescenario ofcomply_test_controller, thedefinition_pin_driftstoryboard, and thedefinition-pinstest vectors.
accept_proposal on drift, so the beta MUST is gone. A buyer that depends on a third-party definition not changing after commitment has no 3.3 guarantee and re-reads the publisher’s adagents.json before accepting. A seller built from a beta bundle that emits pins to a peer on the later schema sends a field a closed commercial_terms rejects.
Purchase rights (published in beta.1, removed after beta.1)
#8101 merged on 2026-10-24 and shipped in3.3.0-beta.1 as experimental media_buy.purchase_rights: purchases[].rights[], the brand/rights-scope.json hoist, and the paid_amplification and content_reuse values of right-use. All three are removed, and brand/rights-terms.json returns to its 3.2 form, including no minItems on uses. A seller that listed media_buy.purchase_rights in experimental_features stops listing it and stops emitting rights[]. Until 4.0, rights travel through the brand protocol, where acquire_rights returns rights-terms.
Installment selection (published in beta.1, removed after beta.1)
#8246 merged on 2026-10-24 and shipped in3.3.0-beta.1 as experimental media_buy.installment_selection: purchases[].installments[], and installment_changes with unsatisfied_installment_changes on refine_proposals. All are removed, along with core/installment-change-set.json and the installment_changes value of proposal_refinement.supported_dimensions. A seller that listed the feature id stops listing it. Installments on the canonical product and holds[] stay in 3.3. A purchase buys the product as offered, which is its complete installment set when it declares one.
Where the designs are kept
The designs are preserved for 4.0 in Planned for 4.0 and in the merge commits of #7960, #8101, and #8246.Required to claim 3.3 behavior
Discovery tasks never provision an account
A seller that provisions accounts lazily, instead of exposingsync_accounts, now provisions only on a provisioning task: one that commits spend or creates account-owned resources, such as create_media_buy, buy_products, accept_proposal, sync_creatives, sync_catalogs, sync_event_sources, or activate_signal. Discovery and negotiation tasks MUST NOT create or activate an account, or accept a seller’s default terms for the buyer. They include:
get_products,list_productsget_signalsrequest_proposals,refine_proposals,decline_proposals
account that does not resolve returns ACCOUNT_NOT_FOUND on these tasks, even where account is optional, instead of being dropped in favor of public results. A seller that provisions lazily instead of exposing sync_accounts MAY answer a discovery task for a natural key it would provision, as if that account existed, without creating it. A seller that exposes sync_accounts returns ACCOUNT_NOT_FOUND. list_accounts is the exception: its account field is a filter, so a key that matches nothing returns an empty list. A seller that provisioned on get_products or on the “first account-scoped request” was conformant under 3.2 and is not under 3.3. Request and response schemas are unchanged; the required side effect is not.
Buyers. Omit account and send brand until the account is provisioned, and provision before request_proposals when you intend to accept. Against a lazily provisioning seller without sync_accounts, send the natural key instead; the seller MAY answer without creating the account, and a provisioning task such as accept_proposal creates it. Recovery from ACCOUNT_NOT_FOUND is the same everywhere: provision a natural key with sync_accounts (or the seller’s lazy path), or verify an account_id. An account_id returned by sync_accounts is accepted as an AccountRef on every account-scoped task.
The advisory storyboard media_buy_seller/unprovisioned_account_reference grades this for sellers that expose sync_accounts. Its checks become required at runner capability 14.1.0. See Account references before provisioning.
Signature verification tightens
Request signing, webhook signing, governance JWSiss, designated-task response signing, and rights attestations now share one agent-resolution algorithm. Some signatures that verified under 3.2 can fail:
- A pinned
signing_keyskey that is not published in the agent’s JWKS is rejected. The pin narrows the accepted keys for sell-side signatures the agent makes about that publisher’s inventory, such as webhooks; it never adds a key and does not apply to the agent’s other traffic. Where a message covers several publishers, the key must match every pin that applies. Publish every pinned key in the agent’s JWKS. - A pin entry without complete public-key parameters for its
kty, such as one with only akid, matches nothing. Matching is by RFC 7638 thumbprint. key_originsis now checked for every webhook signer that publishesbrand_json_url, including pinned keys, and origin binding (the agent URL’s eTLD+1 equals thebrand_json_urleTLD+1, or a House Portfolioauthorized_operators[]entry covers it) applies to webhook signers.- Webhook discovery starts from
identity.brand_json_url. When it is present, buyers MUST use it and MUST NOT fall back. The 3.x fallback reads/.well-known/brand.jsonat the agent’s host, or its eTLD+1, only for sellers that omit the field. Every resolution failure rejects withwebhook_signature_key_unknown. - Agent URLs match by canonical URL on every surface. Webhook discovery and governance
issno longer compare byte for byte, and the schema’sagent_url_matchverifier constraint is nowcanonical. Twoagents[]entries that match canonically are ambiguous only if they differ intypeor in resolved JWKS source; entries that agree on both count once. - Governance buyer identity for signed requests is the exact operator record that agent resolution selected, and
issmust match exactly one governance-typed entry. - Cached or onboarding mappings must be confirmed against the agent’s
brand_json_urland re-confirmed within the brand.json cache lifetime.
A2A operation-resolution hardening becomes MUST
The A2A operation-resolution errata (GHSA-2pm6-6mc8-8xcm) shipped in 3.2.3:required_for, supported_for, and warn_for match the operation resolved from an A2A message’s sole DataPart skill, and an unresolvable request fails closed with request_body_malformed. Its hardening clauses were SHOULD in 3.2.x and are MUST from 3.3:
- strict parsing of unsigned bodies, including JSON-RPC batches and duplicate or case-variant member names, with duplicate keys detected after JSON string decoding (
"skill"and"sk\u0069ll"collide); - A2A Part shape: one content member per Part, no FilePart, and
kindagreement; - task-continuation binding: a
message.taskIdrequest must resolve to the task’s original operation and come from the principal that created it.
hardening tier, by listing a2a_invocation_skill in the new optional request_signing.operation_sources capability. The claim covers every A2A version and binding the endpoint serves, and a present array MUST list every source the verifier supports. The rules are MUST for every 3.3 verifier whether or not it lists the marker. An absent field means the verifier has not declared support: a counterparty MUST NOT infer that required_for is enforced over A2A, and buyers sign every operation in required_for regardless. See Operation resolution over A2A.
Reporting webhooks without operation_id fail closed
reporting_webhook now carries an optional operation_id (1–255 characters from A–Z, a–z, 0–9, _, ., :, -). The seller MUST echo it verbatim into every media_buy_delivery webhook sent to that registration and MUST NOT derive it from the webhook URL. From 3.3, a seller MUST NOT emit media_buy_delivery webhooks to a registration that omits it. The field is expected to become required in 4.0.
On a "3.2" pin the fail-closed rule remains opt-in interim guidance.
Buyers add a unique operation_id to every reporting_webhook registration, and update existing registrations before relying on scheduled delivery. Sellers store it with the registration and, when it is absent, send no media_buy_delivery webhook to that registration and never fabricate a value. 3.2 integrations that adopted the interim agreement already send the same wire shape. See Reporting webhooks.
Reporting and artifact webhooks can omit authentication
reporting_webhook.authentication and create_media_buy.artifact_webhook.authentication are required only on a request served below 3.3 (#8176). When this block is absent, the seller signs every delivery under the profile the registration’s signing_profile selects (adcp/webhook-signing/v1 when it is omitted), as for push_notification_config (Webhook callbacks). No shared secret crosses the wire, so a 3.3 integration can be fully HMAC-free.
Buyers. Omit the block only on a request served at 3.3 or later. A buyer pinned to 3.2 keeps sending it, even to a 3.3 seller.
Sellers. Accept a 3.3-served request without the block and sign its deliveries as for push_notification_config. On a request served at 3.3 or later, the downgrade and injection rules in Webhook callbacks now cover both registrations. A request served below 3.3 still requires the block. Sending it remains valid everywhere in 3.x, and the block is removed in 4.0 (#4288).
Advertise only the measurement terms you accept
A seller MUST NOT advertise, in a product’smeasurement_terms, a measurement_window or other term value that it would reject with TERMS_REJECTED when a buyer replays those terms unchanged on create_media_buy. An advertised window MUST reference a window_id declared in reporting_capabilities.measurement_windows. The measurement_terms_accepted storyboard grades a rejection of replayed advertised terms as a seller failure. See Advertise only what you accept.
Audience identifiers keep their wire form
uids[].value on sync_audiences now has a stated wire form per type. For uid2 and euid, value is the raw UID2 or EUID, never the encrypted UID2 token, and senders and receivers MUST NOT case-fold or re-encode it. A hashed_email entry in uids[] MUST agree with the top-level hashed_email when both are present. For maid, senders SHOULD send the lowercase hyphenated UUID and receivers SHOULD normalize before matching. See sync_audiences.
Delivery rows may omit impressions
impressions is no longer required on get_media_buy_delivery daily_breakdown rows or on the dimensional breakdown rows (by_format, by_device_type, by_device_platform, by_audience, by_demographic, by_installment, by_collection, by_placement, by_property, by_geo, by_keyword, by_creative, by_catalog_item, and their _property variants). Identity keys and spend stay required, and aggregated_totals still requires impressions.
The schema loosens, and the rules around it narrow:
- Receivers MUST NOT treat an absent
impressionsas zero and MUST NOT derive impression-denominated values (cpm,ctr,completion_rate,viewable_rate) for that row. SDK-generated row types make the field optional; handleundefinedorNone. - Senders omit impression-denominated rates on such a row, MUST NOT put views, plays, downloads, or any other count in
impressions, and disclose a committed-but-missing metric throughby_package[].missing_metrics. - Version gate. A response served below 3.3 follows the 3.2 schema, which requires
impressions(Serving an older pin). In it, a seller with no impression count omits the row and never substitutesimpressions: 0.
Product rosters can be undisclosed
A product’spublisher_properties and collections no longer require at least one entry (#7948, refs #6135). A non-empty list is the complete roster, as before. An empty list ([]) now means the seller does not disclose the roster: it is not an empty set and it bounds nothing. There is no new field and no partial state. An absent collections still makes no collection declaration, and publisher_properties stays required on a Product.
- Emission gate. A seller serving a release below 3.3 MUST NOT return a product with an empty roster; the 3.2 schema requires one entry, so it omits the product and returns the others (Serving an older pin). Wholesale feeds and webhooks are not defined by this release.
- Allow lists (
property_list,collection_list,collection_lists) MAY be accepted against an undisclosed roster. The package runs only on the items the seller matches, with no promise to cover every listed item. - Exclusion lists (
property_list_exclude,collection_list_exclude,collection_lists_exclude) are the buyer’s choice, because the buyer sees the undisclosed roster at discovery. A seller MAY accept one, and if it does it MUST apply the exclusion against its full internal roster. A seller that cannot, for example because it does not itself know every publisher it may serve, rejects it withUNSUPPORTED_FEATURE(correctable). - Buyers SHOULD treat an undisclosed roster as opaque in brand-safety and governance logic, decide whether it is acceptable for their exclusions, and MAY refuse such products. Sellers SHOULD offer complete rosters where they can.
Property domain identifiers have an enforceable format
core/identifier.json now validates domain and subdomain values as lowercase
ASCII hostnames with at least two labels, labels of at most 63 characters, and
a hostname of at most 253 characters. IDNs use lowercase A-label/punycode form.
Only domain permits a leading *.. Existing matching rules are unchanged.
Before: the bare string schema accepted Example.com, example.com., and
https://www.example.com/path, so a buyer could submit an exclusion the seller
could not match. After: each value is rejected with INVALID_REQUEST; the
receiver rejects the containing request rather than dropping the identifier.
An invalid identifier in a fetched page invalidates the complete snapshot.
Senders. Validate stored lists before adopting the 3.3 artifact that includes
these constraints. Offer explicit conversion before submission when needed:
Example.com or example.com. becomes example.com, and a pasted
https://www.example.com/path can become www.example.com after the sender
confirms that hostname selection is intended. Validate the converted value;
receivers MUST NOT silently reinterpret it. Apply this to inclusion lists,
exclusion lists, and filters.exclude_identifiers.
This tightens validation without removing a field or changing its type. The
invalid-input rejection precedent in DR-0012
supports rejecting junk input; the newly required lowercase and trailing-dot
rules also require migration of previously schema-valid hostname spellings.
See domain identifier format.
Property list references resolve through AdCP tasks
Before: the spec left the fetch operation and credential use unspecified; an issuer could offer only a customGET /lists/{list_id} route while a seller
called get_property_list over MCP. After: issuers expose get_property_list
over MCP at agent_url, and receivers support that resolution path. Another
advertised AdCP task transport may be used by agreement. A custom HTTP download
route may remain available, but serving it alone does not satisfy the reference
contract, and receivers do not infer its path from agent_url.
Issuers and receivers. When a reference carries auth_token, send it as
Authorization: Bearer <auth_token> to the managing agent for initialization
and every page. The issuer accepts it without a recipient account or second
credential and binds it to read-only access to that list_id and its continuation
pages. It grants no access to other lists, enumeration, or mutations. Fetch and
validate every page before replacing a cached snapshot; authorization or fetch
failure must not remove an inclusion or exclusion constraint.
No reference field is removed or changes type, and Bearer authentication already
exists in AdCP. The new requirements define how the existing list credential is
used and scoped; they do not introduce a replacement authentication scheme.
Issuers, receivers, and SDKs must implement this contract when adopting the 3.3
artifact that includes it; upgrading the schema bundle alone does not supply the
runtime support. DR-0008
preserves shipping SDK wire shapes under ambiguity, not unspecified semantic
contracts. Compliance checks follow the normative resolution requirements under
DR-0001.
See reference resolution
for the complete credential and caching requirements.
Counterparty-asserted domain lists are bounded
media_buy.portfolio.publisher_domains and signals.data_provider_domains are the
only two capability fields whose contents a third party writes: both are populated
from counterparties’ /.well-known/adagents.json, which any domain owner publishes
unilaterally without the named agent’s consent. Unbounded, that let an unrelated
party inflate a victim’s capabilities document past MAX_CAPABILITIES_BYTES — and
because the RFC 9421 signing-key discovery path fetches that document unnarrowed, a
victim that signed under that profile lost signature verification, not just
discovery, one cache cycle later.
The schemas are unchanged; the normative rule is new. A byte budget caps any
single counterparty-asserted field at 8,192 bytes and both together at 16,384,
measured as uncompressed serialized JSON. The structural bounds — at most 100
unique entries, each at most 253 characters — ride in x-adcp-validation and are
graded by the conformance storyboard in 3.x, becoming schema constraints in 4.0.
So a 3.2 response with 101 domains, a duplicate, or an over-length entry stays
schema-valid and is reported rather than rejected. What changes in 3.3 is
conformance: a producer that mirrors a large network wholesale is no longer
conformant, and the fix is to truncate and flag. Recorded in
3.3 exceptions to 3.x compatibility.
Agents. Stop mirroring “every counterparty whose adagents.json names me” — that
is the vector. Publish the publishers or providers you actually represent. If the
real set does not fit, truncate and set the new publisher_domains_complete: false
(or data_provider_domains_complete: false); both default to true, so an agent
whose list is complete changes nothing. Never serve a truncated list as complete.
The bound is a conformance statement, not a runtime control — it makes an
inflated document non-conformant, it does not stop your serializer emitting one.
Buyers. Neither list was ever an allowlist, and both are now explicitly
non-exhaustive: a publisher’s absence is not evidence the seller is
unauthorized for it, with or without the completeness flag. If you prefetch
publisher_domains into an authorization cache, treat a cache miss as “not looked
up yet” and resolve that publisher’s own adagents.json on demand — a buyer that
denies on cache miss will reject legitimate inventory from any seller whose list is
truncated.
Bound that on-demand resolve. products[] and each product’s
publisher_properties[] are sized by the seller, so a naive per-property fetch
hands the seller your fan-out — a larger amplification than the one this change
closes. Resolve a product’s domains in one deduplicated pass, cap the number of
publisher origins and the concurrency, negative-cache failures, and use the
SSRF profile with
the 1 MiB publisher-origin cap rather than a bare fetch — the domain pattern
admits dotless labels like localhost. See
Authorization validation workflow.
Seller revisions advance on every committed change
3.2 text said the media-buyrevision “MUST increment on every mutating state change or update” but listed only update_media_buy, and exempted assignment changes made through sync_creatives and seller-optimized allocation. 3.3 states the rule once: for every resource that carries a seller-maintained revision, the seller MUST advance it on every committed change to state that resource’s authoritative read reports, whatever the origin: an AdCP task, a seller operator, seller automation, another authorized principal, or a connected platform. An externally observed change advances it when the seller ingests it, within the source’s declared freshness bound. Reads, validation-only calls and exact idempotency replays do not advance it.
Sellers.
- Advance the media-buy
revisionon operator and ad-server edits, on connected-platform changes you ingest, on seller automation such as a review decision that changes a package’s creative approval, and on every appliedsync_creativesassignment change (once per affected buy, in the same atomic write as that buy’s assignment change). A platform report that merely echoes a change you already committed is not a new change. Delivery measurements (snapshot,get_media_buy_delivery),indicators,health,impairmentsandwebhook_activitycarry their own freshness fields and are not revisioned state. An ingestion pass that observes no change advances nothing, and one atomic ingestion transaction may coalesce its changes into one revision step. - Append a
historyentry for each such change. For a change that did not arrive through an AdCP task, set the entry’sorigin.kindtoseller_operator,seller_system,connected_platformorunknown, the same vocabulary asaccount.change.origin. The existingactorstring is unchanged. - Seller-optimized allocation: changing a reported cap, target, pacing or lifecycle value advances the revision with
origin.kind: "seller_system". Spreading spend between packages inside the accepted caps remains delivery behavior and advances nothing, so for allocation this restates 3.2 rather than tightening it. - When you maintain a creative
seller_revision,account.change.resource_revisioncarries its value immediately after the change, in the same JSON type.
revision was already rejected with CONFLICT; a buyer that omits revision is not fenced and sees nothing new. The rejection now also happens after a change you did not make, including a sync_creatives assignment from another caller or your own earlier call. Re-read with get_media_buys (the sync_creatives response does not return the new revision in 3.3), reconcile, and retry with the new revision and a fresh idempotency_key; never retry the same stale revision. Do not treat an unchanged revision as proof that package assignments are unchanged on a 3.2 seller.
Flight start activates committed buys only
The 3.x rule that a seller MUST move a buy frompending_start to active when the flight date arrives now applies only to committed buys (confirmed_at non-null). A provisional buy (confirmed_at: null) stays pending_start until the seller confirms or rejects it (#7963). status is the buyer-actionable readiness axis and confirmed_at the seller-commitment axis.
Sellers that hold buys for review never auto-activate a held buy, and can set expected_confirmation_at on get_media_buys while confirmed_at is null. Buyers read confirmed_at, not status, to learn whether the seller has committed. See Provisional buys.
Multi-qualifier metrics omit flat totals
When a buy’s packages report ametric_id under more than one qualifier set, in by_package[].metric_values or in a package’s flat carrier, sellers MUST omit that metric’s flat carrier (the whole nested object, such as viewability) from per-buy totals and every window slice’s totals on get_media_buy_delivery, and from per-buy totals in delivery webhooks (#8055). The rule applies across packages, so one package at mrc and another at groupm no longer combine into one flat viewability. One row is one (scope, metric_id, qualifier) key; impressions and spend are unaffected.
Buyers read the package rows instead of expecting a buy-level value for such a metric. See Media buy delivery object.
Responses conform to the served release
Once you list 3.3 insupported_versions, every release you list is a promise about response shape: Serving an older pin has the rule, which covers get_adcp_capabilities and webhook payloads (DR-0024).
Sellers. Check each response against the published schema of the release you echo, and stop listing a release you can’t serve that way. 3.2 sellers serving a "3.1" pin could emit six shapes a strict 3.1 validator rejects, led by the sync_accounts required-field flip; see the 3.1 to 3.2 list.
Buyers. Keep parsing tolerantly, because sellers that list no release later than 3.2 aren’t bound by the rule.
Refinement carries compliance forward
Abuild_creative refinement (refine_from_build_variant_id) inherits the parent leaf’s compliance set (required_disclosures, prohibited_claims). A refine request can add to it and never remove or narrow it; message and config cannot reduce it. An instruction such as “remove the disclaimer” fails the whole call with COMPLIANCE_UNSATISFIED. A refined leaf gets its own creative_id, and neither seller nor buyer-side approval of the parent extends to it. Schemas gain no field or error code.
Creative agents. If you advertise creative.supports_refinement, retain each leaf’s compliance inputs for the same window you retain the leaf. An agent that retains none can newly fail refinements of briefed leaves. See Compliance and approval across refinement.
Buyers. Re-send nothing: the parent’s compliance carries forward. Treat each refined leaf as unapproved until it goes through your own review.
Platform IDs are emitted under both names
A seller reporting an event source or audience platform ID in a 3.3 or later 3.x response MUST emit bothplatform_id and the deprecated seller_id alias with identical values, including discovery responses and seller-managed event sources. A legacy-only result stays schema-valid, but a 3.3 seller needs dual emission to conform. Buyers prefer platform_id. See Deprecations.
Account identity is the account ID
An account’s identity is the seller-assignedaccount_id (#8474, from #8280 and #8442). A brand is its domain, plus brand_id where its brand.json lists several brands. brand.countries, currency, timezone, sandbox, and operator_unit are account attributes, not identity.
- Sellers MUST return
account_idon every non-failedsync_accountsprovisioning result once the account is resolved, never issue one that might later change, accept{ "account_id": "..." }wherever anAccountRefis accepted, and never change it. If you open buyer-declared accounts throughsync_accounts, accept settings-update entries keyed byaccount_id. Such an entry updates attributes,brand.countriesandbillingincluded, on the same account. Validate a changed attribute as you do when the account opens, and reject a change you do not support withUNSUPPORTED_FEATURE.currency,timezone, andsandboxare set at opening: you MAY refuse acurrencyortimezonechange, andsandboxnever changes. You can review acountrieschange first: reportpending_approvalon the entry’s result row, and keep the current countries until you approve. A natural-key reference resolves only when exactly one of the caller’s accounts matches; otherwise returnACCOUNT_AMBIGUOUS, and MAY list the candidates indetails.candidates. - What narrows for sellers. In 3.2, returning
account_idwas a SHOULD and accepting it was optional. Both are MUST, so a seller that met only the 3.2 wording does not conform. In 3.2, a buyer-declared seller could reject settings-update entries; in 3.3 it accepts them. A provisioning entry never changes an existing account. When it matches exactly one account in your account scope, onbrandandoperatorwith its attributes as filters as therequest_accountdeduplication guidance describes, you SHOULD return thataccount_idinstead of opening another. The exact-or-reject rules forbillingandpayment_termsstill apply. - Buyers store the returned
account_idand send it on every later call. Retry with the sameidempotency_key. Sending a provisioning entry again is safe with a seller that returns the account the entry matches, but that is a SHOULD, so look a lost ID up throughlist_accounts. A change tocurrencyortimezonenormally means opening a new account. - Buyers working with 3.0 to 3.2 sellers. An older seller may reject settings-update entries, may not support a
countrieschange, or may treat countries as part of the natural key. When it can’t take the change, keep using the account reference you have, itsaccount_idor the natural key the seller accepted: the account keeps working with its current countries. Open a new account only if you choose to, with a provisioning entry carrying the new countries. It gets its own reference, and the existing account and its media buys stay as they are. Don’t use the deprecatedoperator_identityrekey to emulate the change. - Countries and accepted buys.
brand.countriesdecides which products, prices, terms, and reviews apply, not where a buy delivers. Narrowing it never changes a media buy the seller already accepted. A change can make two accounts alike; that is not a conflict, and both keep their IDs. A seller that supportsaccount.requestsputs arequest_idon a settings-update row it is reviewing, and the request can resolve to the changed account or an account you already hold. A separate new account comes only from arequest_accountchange that setsallow_separate_account: true; otherwise a seller that would only take the change on a new account rejects it withACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT. See Account identity.
request_account is the single account write where supported
Applies to sellers that declare the experimental account.requests capability (#8293, #8551, #8543 Part A). A seller that does not declare it keeps the 3.2 sync_accounts behavior, and sync_accounts stays stable and available everywhere in 3.3. Its formal deprecation follows account.requests leaving experimental status.
- One write.
request_accountopens an account (brandandoperator) and changes one. With anaccount_idand other fields, each field is a requested change: the brand’scountries(with a matchingdomainandbrand_id),operator_unit,billing,billing_entity, andpayment_terms. Arevisionguards the change, and it is required when the change touchesbilling,billing_entity, orpayment_terms(a stale one returnsCONFLICT).sandboxnever changes.currencyandtimezonechange only on a seller that lists them inaccount.requests.changeable_attributes, and a seller that changes timezone MUST list it; otherwise the change is refused withUNSUPPORTED_FEATURE. - Outcomes. A request is
in_reviewwith a singlepending_actionsaying whose turn it is,resolved,ambiguous, orrejected. A change resolves asupdatedon the named account,matchedto an account the caller already holds, orcreatedas a separate account carrying the change, and in the last two cases the named account is untouched and the seller MUST NOT move media buys, creatives, or reporting between the accounts. Acreatedresolution happens only when the request setallow_separate_account: true(defaultfalse). A seller that would take the change only on a separate account otherwise refuses with the correctableACCOUNT_CHANGE_REQUIRES_NEW_ACCOUNT(#8580, #8582). - Money terms (beta.3). A request that resolves
matchedorcreatedMUST carryaccount, with itsbilling,billing_entity(without write-only fields), andpayment_terms, because they can differ from the terms requested. Anupdatedrequest that changedbilling,billing_entity, orpayment_termscarrieseffective_at, when the new terms take effect. Acreatedseparate account carriesnot_carried_over, the account-level configurations it doesn’t have (#8620). A seller that returned these resolutions withoutaccountfails validation. - Reads.
list_accountstakesinclude_requestsandrequest_idsand returnsaccount_requests[]besideaccounts[]. Requests page throughrequests_paginationandaccount_requests_pagination; the top-levelpaginationpages accounts only. The unreleasedget_account_requeststask is gone, andaccount.requests.read_taskislist_accounts. - Authorization. The seller resolves
account_idagainst the caller’s access before any other check, so an unreachable account answers like an unknown one. A change needsrequest_accountin the grant’sallowed_taskswithread_only: false, and every changed field in the grant’sfield_scopeswhen it lists them (SCOPE_INSUFFICIENT,READ_ONLY_SCOPE,FIELD_NOT_PERMITTED). A newer change request from the same caller supersedes the open one, which is rejected withCONFLICTanddetails.superseded_by. sync_accountswhereaccount.requestsis supported. Buyers SHOULD userequest_account,list_accounts, andsync_principalinstead. Async_accountsentry that carries onlynotification_configsorreporting_delivery_configsis a subscription write: it MUST name an account the caller already holds (otherwiseACCOUNT_NOT_FOUND), and the seller MUST NOT provision, review, or change account attributes for it. A settings update under review carries itsrequest_id.
request_account to the seller that declares it, read resolution (updated, matched, or created; for the last two the resolved account_id differs from requested.account_id), and read account for the terms that apply. Keep using sync_accounts against sellers that don’t declare the capability. See request_account.
Every task response composes the protocol envelope
(#8547, correction (b) in DR-0036.) Every task response schema and async status payload composescore/protocol-envelope.json through its root allOf, as the envelope’s own notes already required. Envelope fields (status, task_id, context_id, context, message, timestamp, replayed, adcp_error, push_notification_config, governance_context) are siblings of the body fields at the response root on MCP and REST. Before, 47 of the 124 response and status schemas did not compose it, and 18 closed roots and branches rejected envelope fields, so generated types could not represent a conforming response.
statusis required. The envelope required it already, and the schemas now enforce it on every response served at 3.3, including the async status payloads and governance responses that omitted it. A webhookresultexample carries the task’sstatus.- Version fields.
core/protocol-envelope.jsoncomposescore/version-envelope.json, soadcp_versionandadcp_major_versionare envelope fields on every response, stamped by the server framework rather than carried as task data. Theadcp_versionecho validates on thecontrol_media_buy,decline_proposals,buy_products, andaccept_proposalresponses. - Open roots. Every response root and status branch is open. The 18 closed ones are reopened: the 3.2 proposal-flow roots and branches, the new 3.3
list_collections,list_proposals,get_media_buy_issue, andreport_media_buy_issueresponses, and the experimentalget_reporting_statusfailure branches. Closed value objects and rows inside a body stay closed. message. A task-levelmessageis at most 2,000 characters, defined once incore/task-message.jsonand treated as untrusted input by the receiver. A seller whose text is longer MUST truncate it rather than fail the task.task_id. One definition,core/task-id.json(type: string,minLength: 1), serves every declaration. An emptytask_idis rejected. A submitted response requirestask_id, a working body requiresstatus: working, and an input-required body requiresstatus: input-required, each through a shared schema.replayed. An absent value meansfalse, and a response may carryreplayed: false;replayed: trueappears only on a replay.
status on every response and truncate message. Buyers and SDK maintainers regenerate types, since envelope fields now appear on every response type, and status enums on the decline_proposals, request_proposals, and refine_proposals roots are replaced by their branch constants.
A major-only request is served the oldest supported release
A request that carriesadcp_major_version and no adcp_version is served the oldest release the seller supports in that major, and the response echoes it (DR-0034, accepted 2026-10-26). For a major-only adcp_major_version: 3 request to a seller that supports 3.0, that is 3.0. Before, the rule was the newest. Every 3.1 and later SDK sends adcp_version, so in practice a major-only request comes from a 3.0 buyer whose payload is 3.0-shaped; answering with the newest release would return a shape its validator rejects. A request that carries neither field is still served the seller’s default release. The rule is 3.x only and ends when 4.0 removes adcp_major_version (DR-0035).
Sellers. Resolve a major-only request to the oldest supported release in that major, and serve and echo it (Serving an older pin). In-house clients that send only the integer lose newer fields; send adcp_version.
Closed schemas stay closed within a major
A schema that any stable release of a major published closed (additionalProperties: false or unevaluatedProperties: false) stays as that release published it until the next major. A minor adds nothing to it, even to peers that negotiated the newer minor, and even for an experimental field (DR-0036, status: proposed, awaiting working-group ratification). Four recorded corrections are the only exceptions, each bringing a schema back to text the spec already required: (a) the 3.2 proposal flow is reopened, (b) the envelope is composed on every response with status required, (c) task_id identifies a task, and (d) message is bounded. Each ships in 3.3 and in a 3.2.x patch.
What this means for 3.3 adopters:
- Features that needed to add terms to
commercial_termsare withdrawn (above), and seller-held change terms are planned for 4.0 instead of shipping ascommercial_terms.change_terms[]. - New 3.3 additions sit on open schemas or in sibling fields.
target_audiences[]is a sibling of the closedaudience_evidence_requirements,vendor_metric_provenance[]is a sibling of the closedvendor_metric_values[]row, andcounting_mechanismsreaches the closed canonical measurement terms in 4.0. - Send a 3.3 field on a reopened schema only to a 3.3 peer. A strict validator on an earlier 3.2 patch still rejects it (Send only to 3.3 peers).
Capabilities are bounded by structure
(#8505, restated in #8526, #8198.) ThemaxItems, maxLength, and maxProperties limits in the get_adcp_capabilities schema, and the x-max-items-4.0, x-max-length-4.0, and x-max-properties-4.0 annotations on fields that were stable before 3.3, are the protocol’s limit on the document. Agents MUST NOT design to a byte figure.
- New and experimental fields enforce the limit with the keywords. Fields stable before 3.3 carry it as the 4.0 annotations, a SHOULD through 3.x that 4.0 turns into the keyword. No schema-valid 3.2 document becomes invalid.
MAX_CAPABILITIES_BYTES(64 KiB) keeps its name and value as a receiver backstop and deployment guidance: receivers commonly bound response size at the HTTP or gateway layer, and a limit near 64 KiB is a reasonable guard against runaway or hostile documents. It is not a design target and not a rule on senders. It stays the fetch budget of step 1 in the deprecated RFC 9421 key-discovery chain through 3.x.- Counterparty-asserted lists are unchanged. The 8,192-byte budget per field and 16,384 bytes together stand, as in above.
Canceled and completed buys keep their packages
(#8467.) From 3.3 a seller retains packages on canceled and completed media buys. It preserves records it held at adoption and packages it later creates or observes, including stable identities, correlation data, platform mappings, and the latest recorded committed configuration whatever its origin. Irrecoverable earlier or never-observed external history is disclosed through nonfatal errors, not by silently returning an incomplete list. Prior terminal package outcomes and the original cancellation metadata are preserved, released creative assignments stay released, and missing statistics are never reported as zero delivery. Responses served as 3.2 permit empty terminal package lists and carry no retention guarantee. A 3.3 stateful conformance scenario checks complete package readback after individual and whole-buy cancellation.Mixed creative approval states at startup
(#8485.) A seller keeps valid pending-review and rejected assignments and starts the eligible approved assignments by default. An optional packagestart_requires_all_creatives_approved gate, declared through media_buy.features.creative_start_policy, holds the start until every assignment is approved, and a structured-only update_creative_start_policy action sets it independently of assignment changes. The gate is consumed at first enablement for serving. A seller distinguishes its own review (pending_start) from buyer-actionable missing coverage or a blocking rejection (pending_creatives). The new pre-activation pending_start to pending_creatives transition needs corresponding SDK transition-validator support before you adopt it. Buyers assign the complete launch set atomically or keep delivery paused during staged supply.
Receivers ignore aggregated_totals in delivery webhooks
Senders still MUST NOT put aggregated_totals in a delivery webhook result. Receivers now MUST ignore it if present, MUST NOT treat it as authoritative, and MUST NOT reject the payload for carrying it. Reconcile totals from get_media_buy_delivery or from the per-delivery rows. See webhooks.
Brand JSON size cap rises to 2 MiB
The brand.json body cap rises from 256 KiB to 2 MiB (2,097,152 bytes) on every fetch path: the AgenticAdvertising.org registry’s live read, and the signing trust-root fetches insecurity.mdx (rights-holder resolution, designated-task response signing, and brand_json_url key discovery). Real brand houses with visual guidelines, colorways, and localized names exceed 256 KiB, so the old cap made them unverifiable. The cap applies to brand.json only. MAX_JWKS_BYTES and MAX_CAPABILITIES_BYTES are unchanged.
This loosens a limit, so interop is asymmetric. A verifier still enforcing 256 KiB rejects a brand.json between 256 KiB and 2 MiB with request_signature_brand_json_malformed (or webhook_signature_key_unknown on the webhook surface), even though a 3.3 verifier accepts it.
Verifiers. Raise the brand.json cap to 2 MiB, and implement the requirements that make the larger cap safe, several of them new MUSTs in 3.3: count bytes while the body streams and abort the connection once the cap is exceeded (never buffer then check, and never read past 2 MiB); apply the cap to decoded bytes when the response is content-encoded; keep the 5 s connect and 10 s total deadlines (the total covers reading the body); enforce a nesting depth of 32 while strict-parsing; send Accept-Encoding: identity; run cheap checks before the fetch and bound concurrent trust-root fetches; and keep the success-cache lifetime bounded by the JWKS revocation polling interval with negative caching at or under 60 s. Revalidating with ETag and single-flighting concurrent fetches keep the larger body from being refetched on every request. SDKs that hard-clamp brand.json to 256 KiB need a release before their verifiers accept the larger size; check your SDK’s changelog.
Operators. A brand.json SHOULD stay under 2 MiB. While counterparties may still be on 3.2 verifiers, an operator whose brand.json carries agents[] or signing keys and needs to verify with them SHOULD keep that file under 256 KiB, for example by pointing identity.brand_json_url at a small Brand Agent variant that carries only the agent entries, until those peers upgrade. The smaller file must still pass origin binding and the key_origins check.
Audience match counts may be absent
In 3.2, aready audience promised matched_count and effective_match_rate. In 3.3 they MAY be absent when the seller cannot report them, and buyers MUST NOT read absence as zero; audience_size and targetable_size carry the size as a range instead. A buyer still on 3.2 expects the counts, so sellers keep populating them, where they can, for callers pinned to 3.2.
Recommended: bound agent response size
Clients SHOULD bound the size of every AdCP response they read, enforce the bound while the body is received rather than after buffering or parsing, apply it to streamed (SSE) responses both per event and cumulatively per call, surface an oversized response as an error rather than truncating it, and default to no more than 4 MiB (4,194,304 bytes). This is a recommendation, not a conformance requirement — a 3.2 implementation does not become non-conformant — but SDKs are adopting the default, so the interop effect is real. See Response size bounds. This tightens a limit, so interop is asymmetric in the opposite direction from the brand.json cap. A buyer that adopts the default rejects a response a buyer without the bound would have buffered and parsed. The rejection arrives as a mid-body connection abort, not as anadcp_error the seller can read off the wire, so a seller whose responses are too large sees a transport failure rather than a protocol error.
Buyers and SDK maintainers. Take the bound from your SDK release rather than hand-rolling it. Check its default against your largest known-good response before enabling it in production; raise it for a specific agent or a specific call, never globally, and never by disabling it. Confirm the SDK enforces while streaming (a buffer-then-check implementation does not close the memory exposure), counts decoded bytes, covers SSE cumulatively, and bounds JSON depth and node count after the byte cap passes — the byte cap bounds ingress, the parse bounds bound memory.
Sellers with large catalogs. Paginate. list_products, get_products, list_creatives, get_media_buys, and the cursor-pageable tools/list catalog all support continuation; the 128 KiB producer interoperability target already applies to each page. A response that only succeeds against a buyer who raised its limit for you is not interoperable. See Managing response size.
Every agent: check your discovery surfaces first. tools/list, get_adcp_capabilities, and the A2A agent card are the first bytes a counterparty reads, and they have no fallback — a buyer that trips its cap there cannot discover you at all, rather than getting a smaller page. Measure all three before you measure anything else. Keep tools/list pages under 128 KiB. Keep get_adcp_capabilities small too. 3.3 bounds it by structure: every array, string, and map has a limit, enforced on new fields and annotated for 4.0 on stable ones. Those bounds are the protocol’s limit, and agents MUST NOT design to a byte figure. Receivers commonly apply a backstop around 64 KiB (MAX_CAPABILITIES_BYTES) at the HTTP or gateway layer against runaway or hostile documents; it is deployment guidance, not a design target. If you sign under the deprecated RFC 9421 profile, its key discovery fetches capabilities under MAX_CAPABILITIES_BYTES (64 KiB), and a document above it makes your signing keys unresolvable as well as your capabilities unreadable. An agent on the Web Bot Auth profile is unaffected, and a creative agent with a large creative.supported_formats catalog should adopt that profile or keep its capabilities under the limit. Treat 64 KiB for the agent card as an editorial target rather than a cap: no normative limit applies to it, and a client must not reject a larger one. Capability-selected tool registration and terse x-tool-summary descriptions are the usual fix. See Discovery responses stay small.
Client response-size default rises to 4 MiB
The recommended client safety limit on a whole MCP message or A2A call rises from 1 MiB to 4 MiB (4,194,304 bytes), matching what the Go, TypeScript, Python and Java SDKs and the hosted grader ship. 4 MiB remains the ceiling on what any default may be. A2A’s per-DataPart check is now the same limit rather than a tighter 1 MiB, because the single-artifact invariant makes the authoritative DataPart the whole response — a tighter per-part number would have become the effective A2A cap and rejected what the byte-identical MCP call accepts. This loosens a limit, so interop is asymmetric. A client still enforcing 1 MiB rejects a response between 1 MiB and 4 MiB that a 3.3 client accepts. A tighter default stays conformant, so there is nothing to fix in such a client — but the response it rejects is one the seller is now entitled to send. Clients. Raise the default to 4 MiB, or keep a tighter one deliberately. Everything that made the bound safe is unchanged and still required: enforce while the body streams, pre-reject an over-limitContent-Length, count raw and decoded bytes, pair the cap with deadlines, cover SSE cumulatively, and bound JSON depth and node count after the byte cap passes. The byte cap is the lesser control — a 4x larger buffer changes little once the node bound rejects at the 100,001st member — but note that the node bound does not limit the length of a single scalar, so per-field length limits are what keep a 4 MiB string out of a log or a model context.
Fan-out clients. This is the change to size before adopting. A per-call default is not an aggregate budget: at 4 MiB and 64 concurrent peers the raw ceiling is 256 MiB, and the resident-object figure is several times that. If your aggregate budget is smaller than concurrency times the per-call default, lower one of the two.
Sellers. The extra headroom is not a licence to grow. The 128 KiB producer interoperability target is unchanged, and while counterparties remain on 3.2-era clients a response between 1 MiB and 4 MiB will be rejected by some of them with no protocol error you can see — the same transition window the brand.json cap change created, in the opposite direction. Paginate instead.
Webhook signing mode and batch bodies
The buyer’s webhook registration selects the signing mode, as in 3.2 (#8475). Anauthentication block asks for the legacy scheme, and without one the seller signs under the registration’s signing_profile (adcp/webhook-signing/v1 when it is omitted). legacy_hmac_fallback is a discovery flag that a buyer checks before asking for legacy signing; it does not select the mode.
- Sellers with
legacy_hmac_fallback: false. Reject a registration carrying anauthenticationblock withUNSUPPORTED_FEATURE, notrequest_signature_required. Pointerror.fieldat the block. - Sellers with
legacy_hmac_fallback: true. 3.2 behavior stays valid through 3.x, including the signature requirement on a registration that carriesauthentication(condition (b)). Condition (b) is deprecated and removed in 4.0 with the block. reporting_webhookandartifact_webhook. Theirauthenticationis optional from 3.3 and still required on a request served below 3.3. These rules apply to them only on a request served at 3.3 or later; below 3.3 nothing changes. An absent block signs under the registration’ssigning_profile(adcp/webhook-signing/v1by default).- Buyers. Send
authenticationonly to a seller whoselegacy_hmac_fallbackistrue. Compare a received webhook’s signing mode with the mode you registered. - Batch bodies. A JSON array body, signed or unsigned, is rejected with
request_body_malformed(SHOULD in 3.2.x, MUST from 3.3, #8064); this replaces the 3.2 interim rule that let a verifier resolve every element of a batch. A client that batches JSON-RPC messages, which older MCP protocol versions allowed, sends one message per request.
Recommended: bound inbound push and webhook bodies
Receivers of A2A push notifications and AdCP webhooks SHOULD bound the request body, the receiver-side counterpart of the response bound above. Enforce the cap while the body streams, pre-reject an over-limitContent-Length, apply the 64-deep, 100,000-member parse bounds after the byte cap and before RFC 9421 or Web Bot Auth verification, apply connect, idle, and total deadlines, and reject with HTTP 413 rather than truncating. The default is 4 MiB, the same number as the attestation task-body cap, and a tighter per-feature cap governs (the 1 MB webhook payload limit still applies to webhook deliveries). See Inbound body bounds.
Receivers. A 3.2 receiver that buffered whatever a seller posted is not wrong under 3.2, but it is the surface this closes. Add the cap in front of signature verification, not behind it, and do not truncate.
Senders. A 413 is a deterministic failure: the same body fails the same way on every attempt, so retrying it on the non-2xx backoff schedule only repeats the cost. Keep webhook payloads well under the cap.
Send only to 3.3 peers
Many 3.3 additions sit inside objects that 3.2 closes, so a strict 3.2 validator rejects them. Send a field or value that 3.3 defines only when the negotiatedadcp_version is 3.3 or later. The cases most likely to bite:
New enum values (
nmo, videoamp, mxf, mpeg2, avc_intra, xavc, print_metrics in available-metric, operator_fee in adjustment-kind) fall under the 3.x rule that clients tolerate unknown values, but a strict 3.2 validator still rejects them; send them only to 3.3 peers too.
Correction: proposal-flow schemas reopened. 3.2 published the list_products, request_proposals, refine_proposals, buy_products, accept_proposal, decline_proposals, and control_media_buy request and response roots closed. It also closed canonical-proposal, canonical-product, product-discovery-criteria, proposal-refinement, package-control, and opportunity-context. DR-0019 keeps roots and containers open. So 3.3 and a 3.2.x patch reopen them, as a correction recorded in DR-0036. Every payload that was valid stays valid, and the rejections closure provided are kept by name. A field that only the legacy Product defines, such as format_ids, is_custom, or product_card, is still rejected on a canonical product. A root feed_version or pricing_version on a request_proposals response, and warnings on an error or submitted buy_products, accept_proposal, or control_media_buy response, are also still rejected. A strict validator on an earlier 3.2 patch still rejects new fields on them, so the rule above applies. commercial_terms and the purchases inside it stay closed.
get_adcp_capabilities runs before negotiation, so a seller cannot gate its capability document. A 3.3 seller’s media_buy.features.catalog_ingestion object fails a 3.2 schema that admits only booleans there. Buyers parse capabilities tolerantly, as Versioning recommends.
What can fail validation
A schema-valid 3.2 message stays schema-valid under 3.3 except for the domain identifier constraints (mixed-case, trailing-dot, and URL-shapeddomain / subdomain values are now rejected) and names 3.3 now defines:
adcp.implementationinget_adcp_capabilities: previously an open extension point, now the SDK implementation shape. Migrate to it or to a vendor-ownedextnamespace.media_buy.features.catalog_ingestion: previously any boolean was admissible; it is now an object that requirescatalog_management: true.dooh_placement_attributes.locationanddooh_inventory_summary: previously open onproduct.jsonand placements, now strict shapes (lon, notlng).ext.adcp: now reserved for the AdCP working group. Vendors that used it for their own data move to their own namespace.reporting_webhook.operation_id: previously an open extra field, now constrained to 1–255 characters from the pattern above.audience_size,targetable_size, andmatch_rate_basison async_audiencesaudience result, if used with a different shape.compact-task-submitted.jsonresponses that break the version-envelope grammar or carrytask_status/response_status(tightened in 3.2.4). Conformant sellers already satisfy both.validate_inputwarning items whosefieldis not a string. The schema always requiredfieldbut never declared it; it is now declared as a string (#8212).
SDK and code-generator maintainers
These change generated types or bundle shape without changing what a conformant peer sends. Items marked 3.2.4 also shipped in that patch; they matter if you generate from 3.2.1–3.2.3.start_hour/end_hourare no longer individually required (#8025). A daypart entry carries exactly one complete pair of eitherstart_hour/end_houror the experimentalstart_time/end_time. Generated types make the stable pair optional, and readers of echoed or proposeddaypart_targetsmust not assumestart_houris present.- Bundled schemas hoist provenance (#7950, 3.2.4).
/schemas/{version}/bundled/carriescore/provenance.jsononce in root$defs.Provenanceand references it from each call site. Bundles shrink by about 20%. Some enclosing inlined schemas lose their deep$id, and tworequired_disclosuresdescriptions may differ. Source schemas and validation are unchanged. - Envelope composition (#7946, 3.2.4). Nine compact and experimental request schemas and
core/compact-task-submitted.jsonnow composecore/version-envelope.jsonthrough rootallOf, so generated types share envelope ancestry. Request validation is unchanged;compact-task-submitted.jsonresponse validation is tightened as listed above. media_buy.featuresis no longer boolean-only (#7927).catalog_ingestionis an object, so generators that emit a map of booleans formedia_buy.featuresbreak.- Daypart requirements (#8025).
required_overlay_support.daypart_targets.timezone_modesis no longer required; a requirement carriestimezone_modes,time_granularity, or both. - Product rosters (#7948, refs #6135).
publisher_propertiesandcollectionsloseminItems: 1onproduct.jsonandcanonical-product.json. Generated types change for every product (a TypeScript non-empty tuple becomesT[]; a Pydanticmin_lengthis dropped). Do not index[0]without a guard, and treat[]as undisclosed, not empty. - Envelope on every response (#8547). Response types compose
core/protocol-envelope.json:statusis required on every task response and async status payload,messageis bounded at 2,000 characters throughcore/task-message.json,task_idiscore/task-id.json(minLength: 1), andreplayedacceptsfalse. Response roots and status branches are open, and 66 local copies of envelope fields are removed so the envelope’s declarations govern. Regenerate types; a hand-written response model that omittedstatusmust add it. - Delivery rows (#8108).
impressionsis optional ondaily_breakdownand dimensional breakdown rows;aggregated_totalsstill requires it. - Reporting issue codes (#8099, 3.2.4). The closed reporting status issue-code enum gains
REVISION_UNREADABLE, with areporting_revision_idfield. Strict readers must accept it. - Spot rows (#8017).
by_spot[].aired_atis required only onairedandmakegoodrows and on rows withoutspot_status. - Settings-update entries. The
sync_accountsSettingsUpdateModebranch permitsbrand,operator_unit,currency,timezone, andbilling, so generated settings-update types gain those five fields.operatorstays forbidden. - New shared schemas.
core/audience-size.json, and the enumsspot-status,daypart-time-granularity,audience-size-precision, andaudience-match-rate-basis. - Conformance vector re-code (#8066, 3.2.4). Request-signing
negative/026-non-ascii-hostnow expectsrequest_target_uri_malformedat step 10, notrequest_signature_header_malformed. Verifiers that matched the old code must update.
Adopt by capability
Optional unless noted. A seller that declares nothing keeps 3.2 behavior.Production security profile (experimental)
Optional. A seller that verifies inbound requests can advertisesecurity_profiles: ["adcp-prod-security-3.3"], listing security.profiles in experimental_features. The profile covers signed spend-committing operations under either signing profile (adcp/request-signing/v1 or web-bot-auth), a 15-minute revocation freshness ceiling with T/3 polling, acts-for checks for the verified signer only, governance binding, least privilege, and outbound fetch safety. In 3.3, rejecting non-conforming counterparties is a SHOULD, and a per-principal spend ceiling is recommended but not part of the id. Buyers that require the profile refuse spend-committing calls when it is absent. See the production security profile.
Seller onboarding posture (experimental)
Optional. A seller can publishaccount.agent_onboarding on get_adcp_capabilities, listing account.agent_onboarding in experimental_features. It gives a mode (open, application, invite_only) per billing party and for sandbox, the requirements for each, and where a human starts. Buyers read it on the unauthenticated capability fetch to tell which sellers take them now, which need a human, and which are not available. The block is seller-wide and identical for every caller. open binds identity, not spend, and needs the Web Bot Auth profile. See account.agent_onboarding.
Stable creative IDs
Creatives that entered a seller’s library outsidesync_creatives, for example before the buyer connected, are listed under a seller-assigned creative_id. A seller that keeps those IDs stable, never reuses a buyer’s ID, and keeps those creatives out of delete_missing declares creative.stable_creative_ids. Without it, 3.2 behavior stands: buyers must not rely on those IDs staying the same, and delete_missing may archive those creatives. list_creatives also returns platform_id, the seller’s own identifier, which was previously only on the sync_creatives response. See Creatives created outside AdCP.
Account change feed records
Only for sellers that declare the experimentalaccount.change_feed. Records whose origin.kind is adcp or seller_operator MUST now carry a non-empty changed_paths (use the root pointer "" when the whole resource was created, deleted, or purged); it stays best-effort for seller_system and connected_platform. A seller that omitted it on those origins in 3.2 must add it before claiming 3.3 behavior for this surface, and buyers should tolerate its absence from a 3.2 seller by treating the change as unknown and rereading the resource. This tightens an experimental surface, so it carries the 6-week notice that experimental status requires. Separately and optionally, a seller can echo the originating request’s idempotency_key on adcp records, only on records served to the principal that sent the request; other readers see the record without it, and notifications never carry it. Buyers use the echo to recognize their own changes and treat its absence as unknown. See list_account_changes.
Installments on the compact product lifecycle
Optional, two pieces with different stability. Stable. The canonical Product gainsinstallments[] of core/canonical-installment.json, closing the 3.2 gap recorded in known limitations. Sellers of scheduled media SHOULD return it so a compact-lifecycle buyer can see which episode, issue, airing, or creator slot a product covers and what its material deadlines are. Buyers request it through fields. Two shape rules differ from the legacy get_products installment: collection_ref is required (the deprecated publisher-domain-free collection_id shorthand is not carried over, and legacy get_products keeps it), and a derivative names its source with derivative_of.installment_ref rather than a response-local installment_id. installments requires collections, so a projection that asks for one gets both. Omitting installments on a product that declares collections still means run-of-collection.
A migration note: a legacy installment that carries only the deprecated collection_id cannot be represented on the canonical surface. Add collection_ref before declaring 3.3 — a seller that drops such installments from its canonical projection is asserting run-of-collection for a product that actually sells named slots.
Not in 3.3. Naming the exact installments a purchase buys, and dropping or swapping one through refine_proposals, was merged for 3.3 and is withdrawn to 4.0. A purchase buys the product as offered.
Creative seller_revision
Optional. list_creatives may return seller_revision on each creative: a seller-maintained, monotonic, opaque token (integer or string) distinct from the buyer-assigned revision_id. Compare values for equality; integers also order numerically. Buyers that see it on a drained account.change record’s resource_revision can tell whether a read already reflects the change. Package, creative assignment, audience, catalog, event source, property list, product and signal revisions are deferred to 3.4. See Seller revision.
Scheduled and human-gated buying
expected_confirmation_at. Set it on asubmittedresponse fromcreate_media_buy,buy_products, oraccept_proposalwhen confirmation waits on a scheduled event; onbuy_productsandaccept_proposal, only for callers negotiating 3.3.refine_proposalscan also carry it on asubmittedresponse to a finalize-only request whose hold waits on a person’s agreement, such as a creator or host (#8104); never on acompletedresponse, and only for callers negotiating 3.3. It is an estimate; buyers check status at that time instead of polling tightly. See Expected confirmation time.- Vendor-metric outcome goals. Plan only
(vendor, metric_id)pairs a product lists in bothvendor_metric_optimization.supported_metrics[]andreporting_capabilities.vendor_metrics, answer invendor_metric_values[], and reject anything else withINVALID_REQUEST. No new capability flag. See Vendor-metric goals. - Governance budget periods (experimental). Governance agents that accept
budget.periods[]onsync_plansdeclaregovernance.supports_budget_periodsand enforce periods incheck_governance. Plans without periods behave as before. See Budget periods. - Hold policy and hold extension (experimental,
media_buy.hold_policy,media_buy.hold_extension). Products and draft canonical proposals can carryhold_policy(available,min_duration,max_duration,extendable,max_total_duration); it binds on the draft proposal and carries forward to the committed successor.refine_proposalsaction: "extend_hold"lengthens a committed hold by returning a new committed successor with the same terms and a laterexpires_at;expected_confirmation_atmay also accompany asubmittedextension (#8203). Both are omitted for callers below 3.3. See Hold policy and Extend a hold. - Delegated composition (experimental,
media_buy.delegated_composition). A requesting platform that owns the catalog can bindcriteria.product_ids_exclusive, soproduct_idsis the complete composable universe; sellers that do not declaremedia_buy.supports_exclusive_product_idsreject it withUNSUPPORTED_FEATURE.media_buy.proposal_refinement.hold_semantics: "terms_only"declares a finalize that commits terms without holding inventory; such a seller never returnscommittedproposals to callers below 3.3. See delegated composition. change_cutoff. Add it to installment deadlines where changes stop taking effect before the booking deadline passes. See Installment deadlines.
Delivery reporting
- Spot lifecycle and break position (experimental,
media_buy.spot_lifecycle). Declaresupports_spot_breakdown.available_statusesandsupports_break_positionbefore sending the fields. Onespot_idtracks fromscheduledtoairedorpreempted; amakegoodrow carriesreplaces_spot_id. Metrics onscheduledrows are expectations. See Spot lifecycle. - Print proof of insertion (experimental). Report
print_metricsper issue onby_installmentrows. Sellers SHOULD populateevidencefor guaranteed print products and report a gap throughmissing_metrics. See Delivery evidence and circulation. - Property-grain rollups. Declaring
media_buy.features.supports_property_breakdownorsupports_installment_property_breakdowncommits the seller to the breakdown on every product with the matching per-product flag, which stays authoritative. Under the property scenarios, the seller builds the breakdown only from rows injected throughsimulate_delivery. - DOOH location and venue summary. Add
dooh_placement_attributes.location(lat,lon,address) to a concrete single-screen placement anddooh_inventory_summary.venue_counts[]to a network product. Eachvenue_countsrow requiresgeo_level,geo_code, andcount, plussystemorcountryas itsgeo_levelrequires. See DOOH. - Audience size. Report
audience_sizeandtargetable_sizeas ranges with aprecision, and omit them when status istoo_small. See Audience size.
Creator and collection cards (experimental)
media_buy.collection_cards adds images, sample_content, and url on collections, brand_ref on talent, distribution[].followers (an account count, never summed across entries), and full collections objects on product and proposal responses when the request sets include_collections: true. brand.collection_links adds collections to get_brand_identity. Copies in responses are never authorization evidence. See Collection cards.
Assignment landing pages and trackers
A seller that declarescreative.supports_assignment_landing_page: true (requires a creative library) accepts landing_page_url on a package creative assignment, sync_creatives assignments[], and assign operations. The override changes the destination for one creative on one package without changing the library creative or its approval; the seller serves it everywhere it applies or rejects the assignment, and returns the actual destination from list_creatives and get_media_buys. Agents without the capability reject the field with UNSUPPORTED_FEATURE. Likewise, a seller that declares creative.supports_assignment_trackers: true accepts assignment-scoped measurement trackers, fires them alongside the creative’s own without changing its revision or approval, enforces the format’s tracker-slot limits where the format declares tracker slots (a format that declares none makes no slot-limit claim, #8713), and returns the assignment-only list from list_creatives and get_media_buys (#8220). See Assignment landing pages.
Hosted video and audio playback trackers
Optional. Thepixel_tracker event enum gains start, first_quartile, midpoint, third_quartile and complete, with the same meaning as the VAST events of the same name, spelled in snake_case. They apply to video_hosted and audio_hosted and are ignored on formats with no playback timeline, as viewable_video_50 is. complete means played to the end whether or not audio was on; audible_video_complete remains the audio-on variant. Tracker execution selectors share the enum, so a seller with a tracker execution contract declares which of them it fires. See Pixel tracker asset for the event definitions and a non-normative Google Ad Manager and FreeWheel mapping.
Buyers sending to 3.2 sellers. A 3.2 schema rejects the new values, so do not send them yet. Send each as event: custom with custom_event_name set to the 3.3 value:
event. A seller with no tracker execution contract silently skips a custom event it does not recognize, so confirm the seller maps the names before relying on them for measurement.
Downgrade to 3.0.x sellers. An SDK that downgrades pixel_tracker to the v1 tracker_pixel shape drops these trackers, including the event: custom interim form above, and emits PIXEL_TRACKER_LOSSY_DOWNGRADE. It never emits them as impression_tracker, because a quartile URL fired at impression time would count every impression as a quartile view.
Linear TV
- Minute dayparts (experimental,
media_buy.daypart_granularity). Declareoverlay_support.daypart_targets.time_granularity(hour,quarter_hour,minute) and reject a finer time withUNSUPPORTED_FEATURE. A seller that does not list the feature id rejects anystart_timeorend_time. See Minute-resolution dayparts. - Broadcast formats and currencies. Use
nmoorvideoampinstead ofcustomindemographic_system.video_hostedacceptsmxfand thempeg2,avc_intra, andxavccodecs; theloudnessobject is experimental (creative.broadcast_loudness). See Broadcast creative.
SDK implementation metadata
Agents can reportadcp.implementation.sdks[] with a package name, exact resolved version, and a components list. components: [] reports SDK use without claiming agent-wide component coverage, such as an SDK signing helper used on only some routes. Existing agents may omit the block; buyers treat omission as unknown SDK use, while sdks: [] explicitly reports no AdCP SDK use. Components describe the deployed server integration, including build-time type imports, rather than the package’s available features. Buyers keep using AdCP version declarations for negotiation and retain all conformance obligations. Approval comes from a trusted catalog, and reducing check depth requires independently trusted evidence covering the endpoint and integration. See implementation metadata.
Targeting errors
A seller that rejects an unsupported or removed targeting dimension withUNSUPPORTED_FEATURE SHOULD use the new error-details/unsupported-targeting-dimension.json details shape (unsupported_dimension, optional supported_dimensions, deprecated, replacement_dimension, product_id, package_index) and point error.field at the targeting path (#8052). No new error code is added; adapters minting UNSUPPORTED_TARGETING_DIMENSION or DEPRECATED_TARGETING_DIMENSION should migrate. See Rejected targeting dimensions.
Open opportunities and later proposals
Optional and experimental (media_buy.open_opportunities). Advertise the parts you implement under media_buy.open_opportunities and list the feature id in experimental_features. list_proposals: true is the read; it is required before later_proposals.supported: true, which also requires max_open_duration. keep_open_after_purchase: true lets accept_proposal and buy_products accept opportunity.status: "open". A seller creating later proposals needs the buyer’s opportunity.later_proposals consent and an open window, honors max_count and min_interval, issues drafts with opportunity_id, expires_at, and a later_reason, and announces them with proposal.created on account notification_configs[], never on a task’s push_notification_config. Buyers send none of these fields, nor an empty decline_proposals declines array, to a seller that does not advertise the matching part; 3.2 sellers reject them. See Open opportunities and later proposals.
age_determination_bases on signals
Optional. Signal listings, definitions, and definition enrichment can carry age_determination_bases (verified, declared, inferred), a sibling of demographic_predicate that says how the signal’s members’ ages were established. It is valid only when demographic_predicate.age is present. A buyer whose request names accepted_bases treats a signal listing as passing the basis test only when its age range is within the request and every one of its bases is accepted; an absent field makes no basis claim. See Age determination basis on included signals.
The field is stable in 3.3, not experimental. It completes the basis vocabulary already stable as accepted_bases, supported_bases, and applied_bases.
Interim on 3.2. Until you claim 3.3, carry the value as ext.adcp.age_determination_bases on the signal-listing entry. This is the AdCP-owned ext.adcp namespace, the same precedent as ext.adcp.opportunity, not a vendor extension, so migrating is a rename: move the value from ext.adcp.age_determination_bases to a top-level age_determination_bases and drop the ext copy. Apply the same matching rule on 3.2. If a listing carries both, the top-level field wins. Buyers cannot infer 3.2 support from absence, and an absent value already means no basis claim, so the interim is safe to omit.
Catalogs, signing, and extensions
- Catalog ingestion. Declare
media_buy.features.catalog_ingestionwithaccepted_catalog_types,ingestion_modes, anditem_status_reporting(required), plussupported_feed_formatswheneverfeed_urlis listed. Declaring it requirescatalog_management: true. Reject an unlisted catalog type, mode, or feed format, or an explicitcontent_id_typeoutside a declared list, withUNSUPPORTED_FEATUREbefore mutation, including ondry_run; inline items beyondmax_inline_items_per_requestfail withINVALID_REQUEST. Buyers treat an absent declaration as unknown, not unrestricted. Seesync_catalogs. request_signing.operation_sources. Listmcp_tools_callanda2a_invocation_skillonly for the sources your verifier implements and passes vectors for, and list every source you support: an array withouta2a_invocation_skilltells peers you do not enforce over A2A. See above.trust.json(experimental).trust/v1/trust.jsonandtrust/v1/trust-acknowledgements.jsonare published underidentity.trust_json(#8067). An agent is identified by the canonical origin of its URL, one agent per origin, and the record carries no keys (#8121). In 3.3 its reader is the experimental Web Bot Auth profile, which derives the record from the agent’s origin, and grants are read atsync_accounts. Publishers declare nothing. See trust.json and the agent identity migration guide.ext.adcp.opportunity. Cooperatingget_productsintegrations can carry an opportunity binding underext.adcp.opportunity, declared by listingadcpinextensions_supported. Durable association needs an authorized account scope, and a mismatched ID on refine is rejected withINVALID_REQUEST. The coreopportunityfield is still in review. See AdCP opportunity extension.
Live creative updates (experimental)
Optional (creative.live_updates, #8488, folding in staged activation from #6780). A buyer changes a creative that is serving by syncing it again under the same creative_id. A seller that lists the feature id declares in creative.live_updates which kinds of change (click_through, trackers, asset_content, format) it applies in place and how the creative serves meanwhile: uninterrupted (decided inside the sync call), staged (the approved prior version keeps serving until the change is approved, then is promoted atomically; on_rejection is keeps_prior or stops), or paused_for_review (the existing lifecycle). Name and tag changes are always accepted.
list_creativesand thesync_creativesitem returnstaged_updatewhile a change awaits promotion;dry_runreturns the exact outcome of one change. There is no per-creative policy readback: the declaration is the general floor anddry_runanswers for one change.- The new notification type
creative.staged_update_changedreports a staged update’s outcome (promotedorrejected) only to subscribers that register it.creative.status_changedis unchanged, so a buyer on an earlier release never receives anapprovedtoapprovedevent that its closed validator would reject. - A seller that declares the capability refuses an undeclared change with
CREATIVE_LIVE_UPDATE_UNSUPPORTED(witherror-details/creative-live-update-unsupported.json) in place ofINVALID_STATE. A seller that doesn’t declare it keeps the original rule: creatives on active, non-paused packages cannot be updated, and the refusal isINVALID_STATE. comply_test_controllerforce_creative_statusforces a staged update’s review outcome. The storyboards aremedia_buy_seller/creative_live_updatesandmedia_buy_seller/creative_live_update_refusal.
Reporting subscriptions (experimental)
Optional (media_buy.reporting_subscriptions, #8543 Part B, #8552, settled through beta.3). Reliable Reporting was configured one account at a time through sync_accounts reporting_delivery_configs, so a new account reported nothing until someone configured it. sync_principal now takes reporting_subscriptions: one registration per feed purpose, scoped to all_authorized_accounts: true (an explicit acknowledgment) or named account_ids, with a cross-seller selector (feed_purpose, period_duration, required_finality, optional alignment and dimensions[], and match). Sellers list accepted registration tasks in reporting_delivery.configuration_tasks[]; configuration_task stays "sync_accounts" for callers pinned below 3.3 and is deprecated. A caller’s own active sync_accounts entry for an account and feed purpose still wins over its subscriptions.
What the buyer can compute from the wire. get_reporting_status reports only exceptions under subscription_coverage, and an exception removes periods from what is owed and never adds any, so a missing report is still an owed period.
- Which periods are owed. A buy is owed every period that ends after its
confirmed_atand overlaps its flight, starting at the first boundary at or after the later of the subscription version’saccepted_atand the account’sauthorization.authorized_at, so the period in which a buy is confirmed is owed in full (#8593). A package added afterconfirmed_atcarriesadded_atonget_media_buys, its ownconfirmed_at: it is owed from the period containingadded_atand resolves its offerings then (beta.4). Sellers that advertise the feature populate it on every package added after confirmation; buyers stop assuming a later package resolves with the buy. The flight stops at the earliest ofend_time,cancellation_effective_at(the delivery stop, later thancanceled_atwhen the seller delivers through a notice period), andcompleted_at. A flight that stops at or before itsstart_timeowes nothing. - Cutovers and billing stubs. A change takes effect as a cutover at instant T. When a billing feed’s incoming schedule has no boundary at the handoff, the stretch to its first boundary is owed as one stub obligation (
stub: true); pacing and analytics feeds leave it unowed. This covers account timezone changes and account-levelsync_accountsbilling configurations (#8617). - Wire inputs.
authorization.regranted_at,flight_historyon media buys andget_media_buys,packages[].reporting_delivery_offering_idsfixed at confirmation,available_fromandwithdrawn_aton offerings, and per-versionconfigurationinprior_versions(#8618, #8588). Offerings publishstandard_dimensionsso the buyer resolvesmatchexactly as the seller does, and sellers MUST publish it on every offering (#8585). - Determinism and scale. Resolution is per package and fixed per buy and version until the resolved offering is withdrawn.
max_accountsadmission is sticky, ordered byauthorization.authorized_atthenaccount_idin byte order. Billing subscriptions MUST usematch: exactwithdimensions.destination_overrides[]route accounts to other destinations, membership edits apply in place asexcluded_by_buyerintervals without a newsubscription_version, and the readback lists 16 prior versions and 1,000 accounts inline, withget_principalpaging the rest throughreporting_subscription_page(#8619). - Deactivation and revocation. A deactivated subscription reads
inactivewith a requireddeactivated_at, and periods that started before deactivation stay owed (#8613). After the caller loses authority for an account,sync_reporting_receiptsandsync_reporting_statustreat every reference to it like an unknown identifier (#8607). A seller that wants a buyer to finish reconciling keeps a reduced grant that lists only the reconciliation tasks, and a grant that listsget_reporting_statusbut not the receipt or status tasks keeps the account covered and refuses the missing tasks withSCOPE_INSUFFICIENT. A grant reduced to tasks withoutget_reporting_statusis a loss of authority at the reduction, exactly like a revocation: the seller lists the account inaccounts_unauthorized[]withunauthorized_atset to the reduction and withholds its coverage, and addingget_reporting_statusback is a re-grant (beta.4). - Period arithmetic. Years, months, and days are civil time; hours, minutes, and seconds are elapsed time. Under a local alignment each local date’s first instant is a boundary, so
PT24HequalsP1D, and a sub-daily duration MUST divide 24 hours.billing_cycleneeds a date component, a local alignment refuses a duration mixing date and time components (P1DT12H; schema validation enforces it from beta.4), weeks (P7D) underutc,account_timezone, andsource_timezonestart on Thursday, andstatus_retention_dayscounts exact 24-hour days (#8588, #8622). - Delivery stays inside the flight. A seller that lists the feature id MUST NOT deliver on a buy outside its flight; to deliver later, including a make-good with remedy
additional_delivery, it first extendsend_time.
static/compliance/source/test-vectors/reporting-subscriptions/ (#8616), and the comply_test_controller reporting_subscription_probe scenario for grants, account timezones, and the virtual clock (#8621). See Reporting subscriptions and Subscription coverage.
Commercial and targeting experiments
Each is optional and experimental: a seller that emits it lists the feature id inexperimental_features, and a seller that declares nothing keeps 3.2 behavior.
- Seller offers (
media_buy.seller_offers, #8102). A seller proposes a change to an accepted buy as acommittedcanonical proposal withproposal_initiator: "seller"and anoffer_reason(installment_postponed,installment_cancelled,preemption,under_delivery,seller_request), amedia_buy_updateormedia_buy_cancellationthat carriesparent_proposal_id,media_buy_id, andbase_media_buy_revision. Buyers read offers throughlist_proposals, announced by the identifiers-onlyproposal.offerednotification, and accept withaccept_proposal, decline withdecline_proposals, or revise withrefine_proposals.expires_atis the response deadline; at it the signed terms stand andaccept_proposalreturnsPROPOSAL_EXPIRED, so silence is never acceptance. A seller that lists the feature id MUST also listmedia_buy.open_opportunitiesand advertiselist_proposals. Sellers MUST NOT send offers to callers served below 3.3. Pre-agreed seller changes insidecommercial_termsare not part of 3.3. See Seller offers. - Price adjustments (
media_buy.price_adjustments, #7955, #7659). A binding fixed-price pricing option (cpm,vcpm,cpc,cpcv,cpv,cpp,flat_rate) can carry an ordered table offeeanddiscountrows, each with arateor a fixedamount, keyed byformat_option,placement_selection(needsoverlay_support.placement_selection), oralways, and optionally avalid_fromandvalid_untilwindow evaluated at the purchasestart_time; a flight that crosses a window boundary is rejected. Selection rows fire at most once per dimension, then each firing row applies to one running total in declared order, rounding half away from zero at every step. A table is optional: without one, the price applies only to what was quoted. A seller never returns a table to a caller below 3.3. See Selection-dependent price adjustments. - Execution readiness (
media_buy.execution_readiness, #7934). For products that declareexecution_requirements, aget_productsrequest withaccountcan setinclude_execution_readiness: true. The response carries anexecution_readinessmap keyed byproduct_id, versioned independently ofwholesale_feed_versionso public wholesale catalogs keepcache_scope: "public". Each product and requirement reportsready,setup_required,selection_required,input_required, orunknown(the seller could not check, which is not the same as the account lacking the resource), with candidates whoseresource_idis directly usable in the existing binding field. See Per-account execution readiness. - Gender targeting (
media_buy.gender_targeting, #7925). Coredemographics.genderpredicates withfemale,male, andnon_binaryvalue sets and an independent unknown group, whoseinclude_unknowncovers both unavailable gender and an explicit choice not to state it. Product-scoped capabilities, exact gender-only and mixed age and gender readback, andsex_gendergovernance classification come with it. Demographic delivery reporting stays age-only in 3.3, so a gender-targeted package has no gendered delivery readback. Buyers MUST NOT send core gender fields on a 3.2 contract. See Core gender targeting. - Signal pricing groups (
signals.pricing_groups, #8058, #7898). An optionalpricing_grouponget_signalsresults and onProduct.signal_targeting_options[], withsignal_pricing_group_rules[]({pricing_group, pricing_combination: highest | each}) on the response and onsignal_targeting_rules, lets a seller price signals such as alternative age bands as one audience. A group with no rule and every ungrouped signal is chargedeach, andflat_fee,per_unit,custom, andpercent_of_mediaoptions are always chargedeach. See Signal pricing groups. - Provider-initiated signal delivery (
signals.delivery_methods, #8044, #6552). A signal agent declaressignals.delivery_methods[]onget_adcp_capabilities, with the same pattern vocabulary as audience activation methods read from the send side. Anactivate_signalagent destination may pin adelivery_method; with no pattern and vendor intersection the destination fails withUNSUPPORTED_FEATURE, and a pin sent to an agent that doesn’t list it is rejected the same way. Agent deployments gaindelivery_status(awaiting_read,delivered,failed), adeliverydescriptor,delivery_error, and an optional match outcome. A receiver field not yet established fails the destination withACCOUNT_SETUP_REQUIRED. See Declaring delivery methods. - Measurement provenance (
media_buy.measurement_provenance, #7940). A sharedenums/collection-mechanism.jsonvocabulary (set_top_box,acr,panel,server_logs,client_tracker,sdk) has two uses, both on open schemas:measurement_terms.billing_measurement.counting_mechanisms[]declares what the billing count is counted from (a seller whose own system is the named vendor SHOULD declare it; absence means undeclared), andvendor_metric_provenance[]on delivery metrics carriesmechanisms[]and an optionaldeduplicatedfor avendor_metric_values[]row, joined on(vendor, metric_id, qualifier). A collection mechanism is notmeasurement_source, which stays the provider slug, and broadcast as-run logs stay airing evidence inby_spot[]. Sellers serving an older pin omit both fields. - Audience evidence targets and invoice lines.
target_audiences[](media_buy.audience_evidence_targets, #8215) names the audience a buyer wants evidence for besideaudience_evidence_requirements, satisfied only by an admissible evidence item for exactly that audience; it is never a targeting predicate.lines[],invoice_kind, andadjusts_invoice_idonget_account_financialsinvoices (account.invoice_lines, #8207) tie an amount to the buys, packages, and reporting revisions it bills, and never settle an invoice.
Experimental surface notices
Experimental creative evaluator drops evaluator_id
Applies only to sellers and buyers that use the experimental creative.evaluator surface (build_creative.evaluator). The evaluator_id source form is removed from core/evaluator-spec.json, and the evaluator entity is removed from x-entity-types. The evaluator object stays the request primitive, with two source forms: inline exemplars or an external agent_url. A seller’s house evaluator is an evaluator agent it lists in creative_policy.accepted_verifiers[]. The list_evaluators follow-on that existed only to resolve evaluator_id is withdrawn.
The seller exposes the evaluator behind an agent that implements
get_creative_features, adds its URL to creative_policy.accepted_verifiers[], and the buyer names that URL. An evaluator carrying only evaluator_id no longer validates, because no source form matches, so a 3.3 validator rejects the whole build_creative request where 3.2 degraded to seller-default ranking. A stale evaluator_id sent alongside exemplars or agent_url still validates (the object stays open) and sellers ignore it. No alias is offered: the id had no on-wire resolution path, and an unknown id already degraded to seller-default ranking.
This is a removal on an experimental surface, which the experimental status contract allows with notice; 3.1 and 3.2 keep evaluator_id. The 6-week pre-landing notice is waived: beta.0 is the notice vehicle, and 3.3 GA ships no earlier than 6 weeks after the beta.0 tag. A fourth source form for conversion-outcome evaluators stays open, and supports_evaluator_gate and hard-gate semantics stay reserved. See Evaluator source model.
Superseded reporting snapshots may be compacted
This change applies to Reliable Reporting (media_buy.reporting_delivery, experimental). status_retention_days keeps metadata for every revision, but content only for:
- the current revision of each logical slice, including every official revision;
- the snapshot it immediately supersedes;
- any superseded snapshot for
automated_recovery_window_secondsor one hour, whichever is longer, after it was superseded; and - for the requesting caller, revisions named by that caller’s own receipts or by its current
received,unreadable, orcontent_mismatchstatus statement.
status_retention_days: 30, an exact read of a pacing snapshot superseded three restatements ago returned its full reporting_rows for 30 days.
After: once that snapshot has been compacted, the same read returns the nondisclosing REFERENCE_NOT_FOUND with field: "reporting_revision_id", exactly as for an unknown ID. Its metadata still appears in get_reporting_status.
Buyers. If you need a snapshot’s rows after it is superseded, persist them when you read them, or record a receipt or status statement that names the revision. Sellers. Retaining the full chain stays conformant. A revision you still owe whose content you cannot serve is a REVISION_UNREADABLE issue, never a not-found. Each window starts at the revision’s created_at, or at its successor’s once superseded; an official revision’s window restarts at its newest adjustment. See revision retention.
Clarifications
These restate existing rules and need no wire change.- Signing buyers serve
get_adcp_capabilities. Request-signature discovery starts at the signer’s agent URL, so a buyer that signs requests answersget_adcp_capabilitiesthere even if it only calls sellers. See request signing. - A2A invocation. An A2A invocation message with more than one DataPart is rejected; the last-DataPart rule applies to responses only. See the A2A guide.
Already shipped in 3.2.2, 3.2.3, and 3.2.4
Adopters of these patches have already seen these; their changesets reappear in the 3.3.0 changelog.- 3.2.2: account references before provisioning, and the trust-docs consistency fixes (only
verify_brand_claimandverify_brand_claimscarry signed payloads; useagents[]instead of the deprecatedbrand_agentandrights_agent). - 3.2.3: the A2A operation-resolution errata with hardening at SHOULD;
authorized_agents[].urlinadagents.jsonis the full endpoint URL including its path, one entry per agent URL; and the storyboard fixes (evaluator feature membership, wholesale and replay capability gates, the pollingsales-guaranteedpath,validate_inputgates, and the acceptance-policy sandbox account). - 3.2.4: the experimental
REVISION_UNREADABLEreporting issue;requires: [controller]on controller-seeded storyboards, so a seller without a controller gets one storyboard-level skip; the storyboard fixes (required billing capability discovery,activenon-guaranteed buys, theadvanced_delivery_reportingjoin key,typed_proposal_negotiationseeding, theseller_optimized_budgetminimum-spend fixture); request-signing vectorsnegative/029–031andpositive/013–015, thenegative/026re-code torequest_target_uri_malformed, completeprofile-3.2/mirrors, and step-5 canonicalization vectors; envelope composition and the bundled provenance hoist; the product channel-declaration guidance; and the principal-layer buyer walkthrough (v3.2.4).
Deprecations
3.3 deprecates two result fields, the principal kind field and assertion, the RFC 9421 signing profiles, one usage, and the account identity-rekey machinery; all remain functional throughout 3.x:seller_idonsync_event_sourcesandsync_audiencesresults. Use the new optionalplatform_idfor the object’s ID in the seller’s ad platform, matchingsync_creativesandsync_catalogs. Sellers reporting a platform identifier in 3.3 and later 3.x responses MUST emit both fields with identical values, including discovery responses and seller-managed event sources. Buyers MUST preferplatform_idand fall back toseller_idonly whenplatform_idis absent. Either field may be absent in legacy responses, and both may be absent when no platform identifier is available. Continue usingevent_source_idandaudience_idin AdCP requests. The aliases keep their object-ID semantics; they are never seller identity.adagents.jsoncontact.seller_idand publisher identifiers of typeseller_idare unchanged. Removal is no earlier than 4.0 and subject to the six-month notice policy. See event source migration and audience migration (#8290).principal_kindandexpected_principal_kindonsync_principalandget_principal, and theenums/principal-kind.jsonenum. A principal is the buyer agent as the seller has bound it, so the kind is alwaysbuyer_agentand carries no information. Sellers keep returningprincipal_kindand honoringexpected_principal_kindthroughout 3.x; buyers stop reading and sending them, and don’t branch onprincipal_kind. Removal is no earlier than 4.0 and subject to the six-month notice policy. The task names are unchanged (#8443, #8280).- The AdCP RFC 9421 signing profiles, for requests (
adcp/request-signing/v1) and webhooks (adcp/webhook-signing/v1), with keys found through brand.json andidentity.brand_json_url. They keep working throughout 3.x and are removed in AdCP 4.0, together with the brand.json trust fields (DR-0023). Their replacement is the experimental Web Bot Auth profile (Web Bot Auth identities; role-by-role steps in the agent identity migration guide): identity is the signer’s origin, keys come from that origin’s key directory, and no capabilities call is needed. Signers that serve no capabilities endpoint can move now. Verifiers keep accepting RFC 9421 signatures through 3.x, under the no-downgrade rule once an origin is bound. - The natural key as the account reference after provisioning. Sending the natural-key
AccountRefon account-scoped tasks once the account exists is deprecated and planned for removal in AdCP 4.0 (#7850). It keeps working throughout 3.x. The natural key itself stays: buyers use it to open accounts withsync_accountsand as alist_accountsfilter to look up an account’s ID. Buyers store the returned ID and send it. See Account identity. - The account identity-rekey machinery (#8474). The
account.identity_updatescapability is deprecated and planned for removal in AdCP 4.0, with everything it gates. That coversoperator_identityanddestination_billing_entityonsync_accounts, theidentity_changeandidentity_change_previewfields, and theACCOUNT_MOVEDandACCOUNT_IDENTITY_CONFLICTerror codes.account.stable_account_idis deprecated with it. An account’s identity never changes, so no rekey or former key exists. Sellers that implementoperator_identitykeep it working through 3.x and SHOULD declarestable_account_id: truewhile the field exists. Buyers changeoperator_unitas an attribute and sendaccount_idwithout checkingstable_account_id.
request_signature_required (#8064), which exists only for the already-deprecated webhook authentication block. It is part of the authentication block’s existing deprecation and is removed in 4.0 with the block. The experimental configuration_task field of media_buy.reporting_delivery is deprecated in favor of configuration_tasks[] and stays "sync_accounts" for clients pinned below 3.3 (#8552). A deprecation window does not reset when a deprecated feature is modified. Other existing 3.2 deprecations keep their published removal windows.
One earlier deprecation is worth restating before 4.0. The integer request field adcp_major_version and the adcp.major_versions capability field, deprecated in 3.1 in favor of adcp_version and adcp.supported_versions, keep working through 3.x and are removed in 4.0 (DR-0035). Buyers send adcp_version on every request and keep the integer mirror until then. Sellers keep emitting adcp.major_versions alongside adcp.supported_versions through 3.x. In-house clients that send only the integer should move to adcp_version now. The v4 compatibility checks flag sellers that still rely on the integer list.
Compliance and grading
version_negotiationgains an optional scenario for 3.1-pinned response shapes. Its checks stay advisory until runners can validate against the served release’s schema bundle.sales-broadcast-tv(sales_broadcast_tv1.1.0) now certifies one authorized TV transaction on either a guaranteed or a non-guaranteed fixed-price path (#8046). Existing claimants should expect new failures: seeded products are demo-pricedcpmwith Live/C3/C7 windows, creatives are assigned throughsync_creativesassignment_operations, and Live and C7 reads must carrymeasurement_window,is_final, andfinalized_at. Claims graded on 1.0.x have a grace window recorded in the compliance catalog. A spot-lifecycle phase runs only for sellers that listmedia_buy.spot_lifecycle.sales-fixed-rateis a new specialism for non-guaranteed, fixed-price sellers in any medium (#7963). Itssales_fixed_ratestoryboard grades capability discovery, fixed-price product discovery, and a bid-lesscreate_media_buy; optional hold phases run only when the seller advertises theforce_media_buy_confirmationcontroller scenario. See Choosing a sales specialism.media_buy_seller/unprovisioned_account_referenceis new and advisory until runner capability14.1.0.media_buy_seller/stable_account_idis new and runs only for buyer-declared sellers that declareaccount.stable_account_id: true. That flag is deprecated in 3.3, so the gate is temporary. It addresses the account byaccount_idonget_media_buysand requires@adcp/sdk15.8.0 or later, whose storyboard runner sends an authoredaccount_idas written.- New capability-gated scenarios:
governance/budget_periods,media_buy_seller/property_delivery_reporting,media_buy_seller/installment_property_delivery_reporting,media_buy_seller/outcome_target_vendor_metric, andmedia_buy_seller/outcome_target_event_cost. Sellers that do not declare the capability skip them. - New experimental scenarios:
media_buy_seller/creative_live_updatesandmedia_buy_seller/creative_live_update_refusal, themedia_buy.execution_readinessscenario, a gender-targeting conformance storyboard, and a stateful package-retention scenario for canceled and completed buys. Thedefinition_pin_driftstoryboard, thedefinition_pin_probecontroller scenario, and thedefinition-pinsvectors are removed with definition pins. - New controller scenario and vectors:
reporting_subscription_probeoncomply_test_controller(revoke_grant,regrant,reduce_grant,set_account_timezone,advance_time), and the canonical reporting-subscription coverage vectors with a reference model. Cross-SDK vectors for undisclosed rosters live understatic/test-vectors/product/. - Hosted service: grading of governance-aware sellers is covered in AAO Verified.
Rollback
Stay on, or return to,"3.2": every 3.3 declaration is optional, so removing it restores 3.2 behavior. The required rules are the exception: they apply as soon as you claim 3.3 behavior (for served-release conformance, as soon as you list 3.3 in supported_versions), so keep a 3.2-pinned route until your discovery, verification, and delivery-webhook paths pass against the beta. A seller that advances revisions from every origin while serving 3.2 callers only makes stale-revision updates fail sooner, which 3.2 already allowed.