
1. What changed, and what did not
If your integration has always sent anthropic-version: 2023-06-01, it has been on the same API version since mid-2023. On 2026-10-21 Anthropic released API version 2026-10-21, the first new version since then, together with new major versions of every official SDK and the ant CLI. The new SDK majors send the new version by default, so the risky moment is not the API release itself but the day someone bumps a dependency.
Two facts frame the work. First, the version is chosen per request by the header, and Anthropic's migration guide states that an SDK sends the API version that was current when its major version was released. A pinned SDK major therefore keeps you on 2023-06-01. Second, the versions page checked on 2026-10-24 lists 2023-06-01 without a deprecation notice or an end date; it only says Anthropic recommends the latest version whenever possible. You can migrate on your own schedule, but recheck that page before you plan around it.
The migration guide prescribes three steps: switch to 2026-10-21, update your code for the breaking changes, and test against 2026-10-21 before you release. It also says to release the SDK upgrade together with the code fixes. This page follows that order, adds an inventory before the switch and ends with a contract test you can run against both versions.
2. Inventory the calls that will return 400
The Messages API changes apply to POST /v1/messages, to token counting and to every request inside a Message Batch created with 2026-10-21. Each one turns a previously accepted request into a 400 error:
| Request shape | 2026-10-21 behaviour | Fix |
|---|---|---|
temperature, top_p or top_k in the body | 400, even at the parameter's default value | Delete the parameter |
A document block placed directly in an assistant message | 400 | Send the document in a user message |
A text block with a non-empty citations array in a user message, including inside search_result blocks | 400 | Remove citations from those blocks |
The exceptions matter as much as the rules. A document nested inside another block, such as a web fetch tool result Claude returned, is still accepted. Citations on Claude's own earlier replies that you send back in assistant messages are still accepted, and so is "citations": {"enabled": true} on documents and search results. A blanket search-and-delete for citations would remove the setting that turns the feature on.
Start with a plain text search of every service that builds requests, including configuration files and prompt templates where a temperature often hides:
rg -n -i "temperature|top_p|top_k" --glob '!node_modules'
rg -n "\"citations\"|citations=|citations:" --glob '!node_modules'
rg -n -i "anthropic-version|x-api-key" --glob '!node_modules'
The third search finds hard-coded versions and code that reads the API key header, which step 5 covers.
Deleting temperature means sampling at the defaults, including a temperature of 1.0, the same as a 2023-06-01 request that leaves it out. On Claude 4.7 and later models, which already accept only the defaults, nothing changes. On a model that accepts other values, such as Claude Sonnet 4.6, a request that lowered temperature for more consistent output loses that effect, and the guide says no other setting replaces it. Its two options are to stop sending the parameter or to keep that request on 2023-06-01. The guide does not describe a per-request version override in the new SDKs, so in practice the second option means that service stays on its current SDK major, or sends the header itself.
3. Inventory Admin API and analytics readers
If you only call the Messages API, skip this step. If you have dashboards, key rotation scripts or cost reports, 2026-10-21 removes the old names of renamed response fields. These are all seven renames in the guide:
| API and endpoints | Old field | New field |
|---|---|---|
| Admin: RBAC groups (create, list, retrieve, update) | roles | role_ids |
| Admin: RBAC group members (list, add, remove response) | group_id | rbac_group_id |
| Admin: RBAC roles (list, retrieve) | name | display_name |
| Admin: organization and workspace rate limits | group_type | group.type |
| Admin: API keys (list, retrieve, update) | workspace_id | scope, with different values |
| Enterprise Analytics: per-user usage and cost reports | actor.email | actor.email_address |
| Enterprise Analytics: activity summaries | summaries | data |
The query parameters keep their names: the group_type filter on rate-limit lists and the workspace_id filter on the API key list are unchanged. Only the response fields move.
The API key change is the one that silently changes meaning. In 2023-06-01, workspace_id is null both for a key bound to the Default Workspace and for an all-workspaces key. In 2026-10-21, scope.workspace_id holds the real ID even for the Default Workspace, and an all-workspaces key has scope set to {"type": "organization"} with no workspace ID. Code that read null as “Default Workspace” must compare against the Default Workspace's ID instead, which you get from List Workspaces with include_default=true. Compare one key under both versions before you change the parser:
for v in 2023-06-01 2026-10-21; do
curl -s "https://api.anthropic.com/v1/organizations/api_keys?limit=5" \
-H "x-api-key: $ANTHROPIC_ADMIN_KEY" -H "anthropic-version: $v"; echo
done
4. Pick the SDK major that sends 2026-10-21
The versions below are, according to the guide and the release notes of 2026-10-21, the first in each language to send the new version. Upgrading below them keeps 2023-06-01; upgrading to them switches every request the client makes, including those sent by the Amazon Bedrock and Google Cloud clients.
| Language | Package | First version sending 2026-10-21 |
|---|---|---|
| Python | anthropic | 2.0.0 |
| TypeScript | @anthropic-ai/sdk | 1.0.0 |
| C# | Anthropic | 13.0.0 |
| Go | github.com/anthropics/anthropic-sdk-go/v2 | 2.0.0 |
| Java | com.anthropic:anthropic-java | 3.0.0 |
| PHP | anthropic-ai/sdk | 1.0.0 |
| Ruby | anthropic | 2.0.0 |
| CLI | ant | 2.0.0 |
Three of these upgrades have a trap that a version bump alone does not catch. In Go, a major version has a new module path, so every import, including subpackages such as option, must start with github.com/anthropics/anthropic-sdk-go/v2, and the new major requires Go 1.25 or later. In PHP, composer update stays within your current constraint, so run composer require "anthropic-ai/sdk". In TypeScript, the v1 platform packages (bedrock-sdk, vertex-sdk, foundry-sdk, aws-sdk, google-cloud-sdk under @anthropic-ai) need @anthropic-ai/sdk v1; leave one behind and your package manager installs a second, older copy of the SDK for it.
For Python, the path runs through 1.0. The Anthropic Python SDK 1.0 migration brief covers that step, httpx2 and all, and recommends pinning anthropic>=1,<2. Keep that pin until the fixes from steps 2 and 3 are merged, then move to anthropic>=2,<3 in the same change that ships them.
Bedrock supports the new API version for models from Claude 4.7 onward. For earlier models, retain the existing SDK or explicitly supplied version header until you change models. If one service also calls the direct Claude API, separate the clients or coordinate both migrations before upgrading their shared dependency.
5. Check what the SDK upgrade changes outside your code
The new SDKs and the CLI send your API key as Authorization: Bearer <key> instead of in X-Api-Key. The Claude API accepts a key in either header, so a direct connection keeps working. What breaks is anything in between that reads or rewrites X-Api-Key: an API gateway, an egress proxy that injects or validates the key, a log filter that redacts only that header, a test stub that asserts on it. The guide gives per-language code for sending X-Api-Key yourself if a gateway cannot be changed; the CLI has no way to do so. Foundry clients are the stated exception and keep sending x-api-key.
A client now also holds exactly one credential. In Python, passing both api_key and auth_token raises a ValueError; with none passed and both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN set in the environment, the client uses the key and logs a warning once. Check CI runners and containers for both variables before the upgrade, not after.
The type changes are smaller than they look. MessageParam becomes a union of UserMessageParam, AssistantMessageParam and SystemMessageParam, and the requests on the wire have the same fields. In Python a dict literal needs no change, but calling MessageParam(role="user", content="Hello") now raises a TypeError. The sampling parameters disappear from the SDK types, Text Completions is removed from the TypeScript, Go, Java and Ruby SDKs and the CLI, the non-beta Models methods lose betas, and the licence changes from MIT to Apache 2.0. A type checker run after the upgrade finds most of this; the header changes it cannot find.
6. Run a contract test against both versions
A mocked client cannot establish the API server's validation behaviour. The script below sends the same probes with both version headers through the base URL you choose. It uses only the Python standard library and sets every header explicitly. It makes 12 small requests to claude-haiku-5-5 unless you set CONTRACT_MODEL. We checked the script locally with a test server, but have not run it against the live Claude API. Its expected statuses are based on Anthropic's documentation, not observed API responses.
"""Contract test: the same probes against API versions 2023-06-01 and 2026-10-21."""
import json
import os
import sys
import urllib.error
import urllib.request
BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com").rstrip("/")
API_KEY = os.environ["ANTHROPIC_API_KEY"]
MODEL = os.environ.get("CONTRACT_MODEL", "claude-haiku-5-5")
OLD, NEW = "2023-06-01", "2026-10-21"
def post(version, payload, auth):
headers = {"anthropic-version": version, "content-type": "application/json"}
if auth == "bearer":
headers["authorization"] = f"Bearer {API_KEY}"
else:
headers["x-api-key"] = API_KEY
request = urllib.request.Request(
f"{BASE_URL}/v1/messages", data=json.dumps(payload).encode(), headers=headers, method="POST"
)
try:
with urllib.request.urlopen(request, timeout=120) as response:
return response.status
except urllib.error.HTTPError as error:
return error.code
def body(*messages, **params):
return {"model": MODEL, "max_tokens": 256, "messages": list(messages), **params}
ask = {"role": "user", "content": "Reply with the word ok."}
reply = {"role": "assistant", "content": "Noted."}
doc = {"type": "document", "source": {"type": "text", "media_type": "text/plain", "data": "Build 42 passed."}}
plain = {"type": "text", "text": "Build 42 passed."}
cited = dict(plain, citations=[{
"type": "char_location", "cited_text": "Build 42 passed.", "document_index": 0,
"document_title": "CI log", "start_char_index": 0, "end_char_index": 16,
}])
# name, request body, auth header, expected status per version (None = not asserted)
PROBES = [
("baseline, Bearer", body(ask), "bearer", {OLD: 200, NEW: 200}),
("baseline, X-Api-Key", body(ask), "x-api-key", {OLD: 200, NEW: 200}),
("temperature at default 1.0", body(ask, temperature=1.0), "bearer", {OLD: 200, NEW: 400}),
("document in assistant message",
body({"role": "user", "content": "Read this."}, {"role": "assistant", "content": [doc]}, ask),
"bearer", {OLD: None, NEW: 400}),
("document moved to user message",
body({"role": "user", "content": [doc, {"type": "text", "text": "Read this."}]}, reply, ask),
"bearer", {OLD: 200, NEW: 200}),
("citations on user text block",
body({"role": "user", "content": [cited]}, reply, ask), "bearer", {OLD: None, NEW: 400}),
("citations removed",
body({"role": "user", "content": [plain]}, reply, ask), "bearer", {OLD: 200, NEW: 200}),
]
failures = 0
for name, payload, auth, expected in PROBES:
for version in (OLD, NEW):
if expected[version] is None:
continue
status = post(version, payload, auth)
verdict = "ok" if status == expected[version] else "FAIL"
failures += verdict == "FAIL"
print(f"{verdict:4} {version} {name}: got {status}, expected {expected[version]}")
print(f"{failures} failure(s) against {BASE_URL} with {MODEL}")
sys.exit(1 if failures else 0)
Save it as contract_2026_10_21.py and run it once directly and once through your gateway:
python contract_2026_10_21.py
ANTHROPIC_BASE_URL=https://llm-gateway.internal.example python contract_2026_10_21.py
7. Read the result against the answer key
The script checks status codes only. A clean run prints 12 ok lines and exits with code 0. Compare a direct API run with a gateway run using the same key, model and payloads. First resolve baseline failures: authentication, model access, rate limits and service errors can invalidate the comparison. The table gives hypotheses to investigate, not diagnoses:
| Failing probe | Most likely cause |
|---|---|
| Bearer baseline fails only through the gateway; X-Api-Key baseline succeeds | Check whether the gateway requires X-Api-Key or strips Authorization. |
| X-Api-Key baseline fails only through the gateway; Bearer baseline succeeds | The tested Bearer path works; check the gateway's authentication policy before retiring any legacy clients. |
| Temperature at 2026-10-21 returns 200 | A proxy may strip the parameter, rewrite the version header or serve its own response. Compare with the direct run and inspect forwarding. |
| Temperature at 2023-06-01 returns 400 while its baseline succeeds | Read the error response and compare the direct run. The result disagrees with the documented default-value behaviour; check the model and gateway validation. |
| A “moved” or “removed” twin fails on 2026-10-21 | The pair has not isolated the intended validation rule. Resolve the positive probe's failure before interpreting the negative row. |
The documented temperature difference gives you a useful comparison: 1.0 is the default, which earlier API versions accept on Claude 4.7 and later models; 2026-10-21 rejects the parameter even at that value. Matching direct and gateway results supports the expected version behaviour. Status codes alone cannot prove which server validated the request. The corrected twins help isolate each negative probe, but inspect unexpected error responses and gateway traces before attributing a 400 to the version change.
Finish by capturing a handful of real request bodies per service from logs or fixtures, with the corrections from step 2 applied, and replay them with post(NEW, payload, "bearer"). Then test those calls through the upgraded SDK: this script sends raw HTTP and does not exercise SDK types, serialization or authentication changes. After deployment, keep the probes in CI; retain both versions if you want the comparison to continue detecting a gateway that pins the old version. For the rest of building on the API, see the Claude API developer guide.
Sources checked 2026-10-24
- Anthropic: Migrating to API version 2026-10-21 — upgrade steps, per-language SDK versions, the Bedrock warning, the Messages API breaking changes and their exceptions, Admin API and Enterprise Analytics renames,
workspace_idon API keys, Bearer authentication, one credential per client,MessageParamunion and other SDK changes. - Anthropic: API versions — version history (2026-10-21, 2023-06-01, 2023-01-01), recommendation to use the latest version, no end date for 2023-06-01.
- Anthropic: Claude Platform release notes, October 21, 2026 — release of API version 2026-10-21 and the SDK and CLI versions that send it by default.
- Anthropic: Model deprecations — sampling parameters removed in 2026-10-21, rejected by earlier versions only at non-default values on Claude 4.7 and later models;
claude-haiku-5-5listed as active.