> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Version Adaptation

> Three version axes move at the same time when you ship an AdCP agent — spec version, SDK version, and per-peer version. SDKs ship three concrete mechanisms (per-call pinning, co-existence imports, on-wire negotiation) so adopters don't carry the translation matrix in handler code.

Three versions move at the same time when you ship an AdCP agent or client:

| Axis                        | Example                       | What changes                                          |
| --------------------------- | ----------------------------- | ----------------------------------------------------- |
| **Spec version**            | AdCP `2.5 → 3.0 → 3.1`        | Wire shapes, error codes, lifecycle states, new tools |
| **SDK version**             | SDK `5.x → 6.x`               | API surface, ergonomics, compile-time guarantees      |
| **Peer version** (per call) | Buyer at v3.0, seller at v2.5 | A single conversation crosses versions                |

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](/dist/docs/3.0.24/building/cross-cutting/sdk-stack#version-adaptation). For the spec-side rules see [Versioning](/dist/docs/3.0.24/reference/versioning).

<Warning>
  AdCP v2.5 is end of life. The v2.5 examples below describe a temporary migration bridge for existing integrations, not a supported production configuration. New integrations must use v3.
</Warning>

## 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`):

```ts theme={null}
import { ADCPMultiAgentClient } from '@adcp/sdk';

const client = ADCPMultiAgentClient.simple(
  'https://legacy-agent.example.com/mcp/',
  {
    auth_token: process.env.AGENT_TOKEN,
    adcpVersion: 'v2.5', // ← pin here
  },
);

const agent = client.agent('default-agent');
const result = await agent.getProducts({
  idempotency_key: '550e8400-e29b-41d4-a716-446655442052',
  buying_mode: 'brief',
  brief: 'CTV inventory',
});
```

Python and Go SDKs expose the same mechanism under their idiomatic call sites — see each SDK's repo. The shape is consistent: per-agent or per-call version pin, validated at construction time, with adapter modules translating to/from the canonical shape transparently.

### 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/`](https://github.com/adcontextprotocol/adcp/tree/main/dist/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 broad `get_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.

| Current SDK method | Legacy peer tool                           | Fallback mode  | Required behavior                                                                                                                                                                         |
| ------------------ | ------------------------------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `listProducts`     | `get_products` wholesale                   | `orchestrated` | Translate the common case directly; when exact `product_ids` are requested, page and filter until every requested ID is found or the feed is exhausted                                    |
| `requestProposals` | `get_products` brief                       | `orchestrated` | Return immutable draft snapshots with source IDs preserved                                                                                                                                |
| `refineProposals`  | `get_products` refine or proposal finalize | `orchestrated` | Mint draft revisions or committed held snapshots with source-result correspondence and atomic finalize batches                                                                            |
| `declineProposals` | none                                       | `none`         | Throw a typed capability error before sending a call; never silently discard terminal feedback                                                                                            |
| `buyProducts`      | `create_media_buy` explicit packages       | `orchestrated` | Translate product purchases to packages without adding creatives, then materialize the accepted proposal snapshot expected by the current result                                          |
| `acceptProposal`   | `create_media_buy` proposal mode           | `orchestrated` | Execute a new-buy proposal directly; amendment/cancellation proposals require an SDK-managed compatibility workflow or a typed capability error when the legacy seller cannot preserve it |
| `controlMediaBuy`  | `update_media_buy`                         | `orchestrated` | Map only canonical operational fields and never route creative, flight, package-addition, or billing-term mutations through this adapter                                                  |

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`.

For proposal finalization, the adapter MUST retain a durable mapping from the current successor `proposal_id` to the legacy source ID that `get_products` finalized. 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.

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.

## 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:

```ts theme={null}
// v5 code — change only the import path
import { createAdcpServer } from '@adcp/sdk/server/legacy/v5';

serve(() => createAdcpServer({
  name: 'My Agent',
  version: '1.0.0',
  // …existing v5 handler bag — unchanged
}));
```

Greenfield code in the same project uses the v6 entry point side by side:

```ts theme={null}
import { createAdcpServerFromPlatform } from '@adcp/sdk/server';

const platform = new MyPlatform(); // implements DecisioningPlatform
const server = createAdcpServerFromPlatform(platform, {
  name: 'my-agent',
  version: '1.0.0',
});
```

Both compile, both run, both pass conformance. You migrate one handler — or one specialism — at a time. The legacy subpath is a documented co-existence path, not a deprecation warning.

Other-language SDKs follow the same pattern: prior-major surfaces remain importable from a versioned subpath alongside the current entry point. Check each SDK's release notes for the specific import paths.

### When to actually migrate

Stay on the legacy surface as long as it keeps compiling and passing [conformance](/dist/docs/3.0.24/building/verification/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):

```ts theme={null}
import { createAdcpServer } from '@adcp/sdk/server';

const server = createAdcpServer({
  name: 'My Agent',
  version: '1.0.0',
  capabilities: {
    major_versions: [3],
    supported_versions: ['3.0', '3.1'],
    // …other capability fields
  },
  // …handlers
});
```

The union of `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](/dist/docs/3.0.24/reference/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 an `adcp_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](/dist/docs/3.0.24/reference/versioning#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 typed `VersionUnsupportedError` (or language equivalent) *before* sending the request. Catch it from the call site:

```ts theme={null}
import { VersionUnsupportedError } from '@adcp/sdk';

try {
  const result = await agent.getProducts({
    idempotency_key: '550e8400-e29b-41d4-a716-446655442053',
    buying_mode: 'brief',
    brief: '…',
  });
} catch (err) {
  if (err instanceof VersionUnsupportedError) {
    // peer doesn't support this call at the pinned version —
    // re-pin adcpVersion or switch agents
  }
  throw err;
}
```

**2. `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`:

```ts theme={null}
const result = await agent.getProducts({
  idempotency_key: '550e8400-e29b-41d4-a716-446655442054',
  buying_mode: 'brief',
  brief: '…',
});

if (!result.success && result.adcpError?.code === 'VERSION_UNSUPPORTED') {
  const supported = result.adcpError.details?.supported_versions ?? [];
  // pick a version you also support, then re-issue with adcpVersion pinned
}
```

`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:

1. **Server**: declare `supported_versions: ['3.0', '3.1']` in capabilities. The SDK accepts both on the wire and returns `VERSION_UNSUPPORTED` to anyone outside the set. (Only declare a version your handlers actually satisfy.)
2. **Client (per peer)**: pin `adcpVersion` to 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.
3. **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.

The combined effect: **one handler codebase, three version axes, no fork.**

## 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_version` parsing, `adcp_version` cross-checks, `VERSION_UNSUPPORTED` envelope shaping with the supported-versions echo).
* Keep its conformance test surface in sync as new versions ship.

Each of these compounds at every spec revision. SDKs absorb them so your team's effort goes into L4 differentiation, not into versioning plumbing.

## 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_key` required on every mutating tool, with the `replayed: true` / `IDEMPOTENCY_CONFLICT` / `IDEMPOTENCY_EXPIRED` semantics declared on `get_adcp_capabilities`. See [Idempotency on Calling an agent](/dist/docs/3.0.24/protocol/calling-an-agent#idempotency-replay-vs-new-operation).
* **Published lifecycle state machines** — seven resource types (`MediaBuy`, `Creative`, `Account`, `SISession`, `CatalogItem`, `Proposal`, `Audience`) with legal-edge enforcement and the `NOT_CANCELLABLE` / `INVALID_STATE` precedence.
* **Conformance test surface** — [`comply_test_controller`](/dist/docs/3.0.24/building/by-layer/L3/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` / `terminal` recovery 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.

For the full 3.0 changelog (protocol-wide, not just L3), see [What's new in v3](/dist/docs/3.0.24/reference/whats-new-in-v3). For the migration path, see [Migrate from a hand-rolled agent](/dist/docs/3.0.24/building/by-layer/L4/migrate-from-hand-rolled).

A from-scratch 2.5 agent was tractable; a from-scratch 3.0 agent is the [3–4 person-month L3 build](/dist/docs/3.0.24/building/cross-cutting/sdk-stack#why-sdks-matter-more-in-adcp-than-in-eg-http) decomposed in the SDK stack reference. SDKs exist because L3 grew faster than implementers could hand-roll.

## See also

* [The AdCP stack](/dist/docs/3.0.24/building/cross-cutting/sdk-stack) — layered architecture reference
* [Where to start](/dist/docs/3.0.24/building) — decision page
* [Versioning](/dist/docs/3.0.24/reference/versioning) — spec-side version rules
* [What's new in v3](/dist/docs/3.0.24/reference/whats-new-in-v3) — protocol-wide 3.0 changelog
* [Migrate from a hand-rolled agent](/dist/docs/3.0.24/building/by-layer/L4/migrate-from-hand-rolled) — when adopting a stack with mid-flight buyers on different spec versions
