Official SDKs ship three concrete mechanisms so adopters don’t carry the translation matrix in handler code. This page is the recipe per mechanism. For the conceptual background see the SDK stack — Version adaptation section. For the spec-side rules see Versioning.
Mechanism 1 — Pin the spec version per call
Use this when you’re a client talking to a peer that’s pinned to an older (or newer beta) spec version. The SDK runs your request and the peer’s response through adapter modules so your handler code stays on the canonical (current) shape.Pin the version on a single agent
JavaScript / TypeScript (@adcp/sdk):
Validate the version up front
adcpVersion (or the language equivalent) is validated at construction time. The SDK only accepts versions whose schema bundle ships with the build — if the bundle isn’t present (e.g., you pinned a beta channel that hasn’t been synced into your installed SDK), construction throws a typed configuration error with a pointer to the schema-sync tooling.
To see what your installed SDK actually has bundled, query the SDK’s compatibility-list export — every official SDK exposes one. For the spec-side authoritative list of what each AdCP version means on the wire, see schemas/.
What the adapters actually do
Each SDK ships per-tool adapter modules — pure shape translations (field renames, default population, structural reshaping). The SDK applies them transparently when the version pin is set; your handler sees the current shape regardless of which version the peer speaks. When AdCP 3.1 ships and you bump the SDK, a new adapter folder appears for the now-legacy 3.0. Your handlers don’t move.Same-major tool replacements
AdCP 3.2’s compact lifecycle replaces the broadget_products, create_media_buy, and update_media_buy facades with narrower tool names. This is not ordinary version-shape adaptation: some current operations need more than one legacy call, and one has no faithful legacy equivalent. The release manifest makes that distinction machine-readable through legacy_fallback on each replacement and superseded_by on each compatibility facade.
Client SDKs SHOULD expose the current methods as their primary API and choose the peer path from
media_buy.lifecycle_tools. A directly advertised current tool always wins. A direct fallback may run transparently. An orchestrated fallback may run transparently only when the SDK ships a handwritten adapter that preserves the current tool’s result completeness, terminal state, atomicity, and idempotency semantics; the manifest classification does not synthesize that state machine. In particular, SDKs adapting a direct 3.x creation MUST retain the accepted commercial snapshot locally when the legacy seller cannot return it, and MUST not pretend a legacy seller supports proposal-based amendments. Each underlying legacy mutation needs its own deterministic replay identity, and retrying the current SDK call must resume rather than restart the sequence. Otherwise the SDK MUST throw a typed CapabilityUnsupportedError carrying requested_tool, the peer’s advertised tools, and fallback_mode.
orchestrated is conditional, not a promise that every valid compact payload can
be sent to every earlier 3.x peer. Adapters MUST preflight each structured field
and fail before calling the peer when the older version has no exact
representation. They MUST preserve legacy incomplete[], time-budget, and
product-pagination semantics instead of turning a partial result into complete
success. In particular, 2.5 has no wholesale-feed contract, while 3.0 has
wholesale pagination but no seller-issued feed or pricing fence. Released 3.1
can support a normal listProducts projection only when the seller returns a
real scope-correct wholesale feed version and all requested criteria map
exactly.
Products-only brief results
A valid 2.5, 3.0, or 3.1 brief may return useful products and no proposals. Compact compatibility preserves that result asproducts_available. This is a
projection-only compatibility outcome: native 3.2 sellers do not emit it, and
ordinary request_proposals success still contains a genuine proposal.
Its purchase_continuation is discriminated:
listed_purchaseis available only after the coordinator re-reads the exact selected products through a real seller-issued, account-scoped feed and obtains its feed and pricing fences. Those seller values flow unchanged intobuyProducts.legacy_createroutes to established explicit-packagecreate_media_buywhen no truthful fence exists. It declaresfeed_version_not_atomicandpricing_version_not_atomic. For AdCP 2.5 it additionally declaresmutation_idempotency_not_guaranteed; 3.0 and 3.1 declare that loss when the actual peer cannot provide their mutation replay guarantee. It fails closed unless the caller explicitly accepts every returned loss before mutation.
continueLegacyPurchase operation using the generated
CompatibilityPurchaseCoordinatorInput type. The input carries a coordinator
idempotency key, the opaque token, the bound account, selected product IDs, the
exact accepted loss set, and the proposed source-version create_media_buy
payload. It is not an AdCP tool payload. The coordinator validates every
binding, exact consent, expiry, source-version request schema, and token claim
before sending only the legacy create request. Exact retries resume durable
state; crash-ambiguous mutations fail unless they can be reconciled
authoritatively.
The coordinator never promotes products into a feed on the seller’s behalf and
never synthesizes feed_version, pricing_version, a proposal,
commercial_terms, or terms_digest. A projection token may bind the observed
response to the caller, account, and selected payload, but it is not a
seller-issued atomic fence. The legacy seller still revalidates availability
and price during create. This compatibility outcome and its legacy continuation
are deprecated in 3.2 and removed in AdCP 4.0.
In the reverse direction, a 3.2 seller advertising established
get_products and create_media_buy facades keeps products returned by a
products-only brief executable through that same legacy create facade. An
established buyer is never required to understand products_available or an
SDK-local continuation token.
For every proposal revision and finalization, the adapter MUST retain a durable mapping from the current successor proposal_id to the legacy source ID. A later acceptProposal sends the mapped legacy ID to create_media_buy while returning and retaining the current immutable successor snapshot. The mapping is part of the adapter’s replay state: losing it, reusing the source ID as the current ID, or minting a second successor on retry violates the 3.2 contract. A legacy proposal may be projected only when the adapter can source every required purchase price, resolved flight, brand, and other canonical term. Valid legacy proposals may omit those facts; the adapter returns products_available or a typed capability error rather than filling the gaps itself.
Simple free-text refinement and product include/omit operations may map when
their result is a complete successor. Typed budget, CPM, impression, flight,
alternative-count, criteria-replacement, amendment, and cancellation semantics
need exact adapters or a pre-mutation failure. Legacy more_like_this maps to
free text only with explicit lossy consent. Legacy proposal omit is response
filtering and MUST NOT be interpreted as terminal declineProposals; no legacy
decline fallback exists.
Released legacy get_products does not provide the compact tasks’ required
mutation-idempotency contract. Durable local replay state is necessary but is
not sufficient when a process can crash after the seller commits and before the
adapter records the response. Unless the adapter can authoritatively reconcile
that state, it MUST fail the fallback rather than claim compact exactly-once
behavior.
These tools deliberately retain distinct operation_family values. Fallback metadata describes SDK adaptation, not cross-name authorization, idempotency equivalence, task recovery, or webhook identity. A client adapter MUST be authorized for the actual legacy call; a grant for a split tool does not transfer across names. Server SDKs may implement the current lifecycle once and derive the deprecated facades for 3.x callers, but may advertise only behavior the adapter actually preserves.
The complete version and transaction-boundary matrix is in
specs/legacy-compact-lifecycle-compatibility.md.
Mechanism 2 — Migrate SDK majors via co-existence
Use this when you bump your SDK from one major to the next and don’t want to rewrite every handler the day you upgrade. Each SDK keeps the prior major’s surface available alongside the new entry point.Example: @adcp/sdk 5.x → 6.x
In v6.0, the v5 entry point was hard-removed from the top-level export. Existing v5 code keeps working by swapping one import path:
When to actually migrate
Stay on the legacy surface as long as it keeps compiling and passing conformance. Migrate a specialism when you want the new features (compile-time specialism enforcement, capability projection, idempotency / signing / async-task / status-normalization pre-wiring on greenfield code). There’s no rush.Mechanism 3 — Wire-level negotiation
Use this when you’re a server and you want to be explicit about which spec versions you accept.Declare what you support
supported_versions (release-precision strings) and/or major_versions go on your agent’s capability declaration. Use release-precision strings — '3.0', '3.1' — not the legacy aliases ('v2.5', 'v3') used for client-side pinning. A 3.x server with no v2.5 handler logic should not declare 'v2.5' here — its 2.5 callers go through client-side adapters at the buyer end, not the server’s accepted-version set. If you want to surface your deployment’s patch build for incident triage, use the build_version capability field — not supported_versions.
Example with @adcp/sdk (lower-level handler-bag API):
supported_versions (parsed to majors) and major_versions defines the seller’s accepted set on inbound adcp_major_version / adcp_version claims. See Versioning — version negotiation for the spec rules and the bidirectional negotiation flow introduced in 3.1.
What happens on a mismatch
If a buyer’s request carries anadcp_major_version (or adcp_version) that isn’t in the accepted set, the SDK returns a VERSION_UNSUPPORTED error envelope. The envelope echoes the seller’s supported_versions so the buyer can downgrade their pin without an out-of-band lookup. See VERSION_UNSUPPORTED error data for the envelope shape.
Buyer side: two surfaces
There are two places version mismatch can surface on the client, and they fire in different conditions: 1. Pre-flight typed exception. When the client already has the peer’s capabilities cached and knows up front that the call won’t go through, the SDK throws a typedVersionUnsupportedError (or language equivalent) before sending the request. Catch it from the call site:
VERSION_UNSUPPORTED envelope from the wire. When the mismatch is only detected on the server side (e.g., the buyer’s adcp_major_version parses different than the buyer’s adcp_version string), the response carries a typed VERSION_UNSUPPORTED error envelope that echoes the seller’s supported_versions:
VERSION_UNSUPPORTED is recovery-classified correctable — clients that handle it programmatically retry against a supported version.
This is the third mechanism rather than a fallback to the first: negotiation tells you what’s possible; per-call pinning tells the SDK which one to use.
Putting it together
A supported multi-version production setup, with an optional migration-only bridge for an end-of-life v2.5 peer:- Server: declare
supported_versions: ['3.0', '3.1']in capabilities. The SDK accepts both on the wire and returnsVERSION_UNSUPPORTEDto anyone outside the set. (Only declare a version your handlers actually satisfy.) - Client (per peer): pin
adcpVersionto a supported release based on what the registry or peer’s capabilities advertise. Use'v2.5'only as a short-lived migration bridge for an existing end-of-life peer; the client-side adapters translate its wire shape so your application code stays on the current spec. - SDK upgrades: bump the SDK on your schedule; switch to the new entry point per specialism over time; keep the rest on the legacy import until you’re ready.
What this saves you from building
A from-scratch agent has to:- Maintain a translation matrix between every spec version it claims to support, and update it every time a release ships.
- Hand-roll API stability across its own internal refactors.
- Implement the negotiation handshake (
adcp_major_versionparsing,adcp_versioncross-checks,VERSION_UNSUPPORTEDenvelope shaping with the supported-versions echo). - Keep its conformance test surface in sync as new versions ship.
What changed at L3 in 3.0
If you’re scoping a hand-rolled agent against today’s spec, the L3 surface added with AdCP 3.0 is the largest delta from 2.5. Most of what an SDK does at L3 didn’t exist as a published primitive before 3.0:- Mandatory idempotency —
idempotency_keyrequired on every mutating tool, with thereplayed: true/IDEMPOTENCY_CONFLICT/IDEMPOTENCY_EXPIREDsemantics declared onget_adcp_capabilities. See Idempotency on Calling an agent. - Published lifecycle state machines — seven resource types (
MediaBuy,Creative,Account,SISession,CatalogItem,Proposal,Audience) with legal-edge enforcement and theNOT_CANCELLABLE/INVALID_STATEprecedence. - Conformance test surface —
comply_test_controller(sandbox-only) so storyboards drive state deterministically. Replaces ad-hoc per-seller test endpoints. - RFC 9421 signatures as a baseline — optional in 3.0, mandatory under AAO Verified. Replaces the loose-bearer-token posture of 2.5.
- Expanded error catalog with recovery classification — 18 standard error codes with
transient/correctable/terminalrecovery semantics. Hand-rolled 2.5 agents typically returned unstructured error strings. - Async-task contract — every mutating tool can be sync or async; the contract for which terminal artifact closes the task is specified.
- Webhook signing — push notifications signed with the same RFC 9421 profile as outbound requests; replay-window + retry semantics specified.
See also
- The AdCP stack — layered architecture reference
- Where to start — decision page
- Versioning — spec-side version rules
- What’s new in v3 — protocol-wide 3.0 changelog
- Migrate from a hand-rolled agent — when adopting a stack with mid-flight buyers on different spec versions