Skip to content

Beta Headers in the Claude API: How Features Ship Before General Availability

8 min read · updated August 11, 2026

Anthropic ships new API surface behind an opt-in request header rather than behind a new endpoint or a new API version. The consequence worth internalising: two identical request bodies can behave differently, and the difference is one header you cannot see in a JSON log.

Three headers, three jobs

These get conflated constantly, so it is worth separating them before anything else.

  • anthropic-version — required on every request, and stable. The published value has been 2023-06-01 since the Messages API launched. It pins the request and response format, so that a future breaking change to the schema cannot break existing clients. Omitting it is an immediate 400.
  • anthropic-beta — optional, and this page’s subject. It opts one request into one or more named pre-GA features. Its values are dated feature names such as interleaved-thinking-2025-05-14, not version dates.
  • The model ID — not a header at all, but it is the other half of the same question, because a beta is only meaningful on models that implement it. A dated model ID like claude-sonnet-4-20250514 is what makes a beta’s behaviour reproducible. See pinning a Claude model version.

Syntax and multiple betas

The header takes a comma-separated list. Multiple betas in one request go into one header, not into repeated headers:

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: token-efficient-tools-2025-02-19,interleaved-thinking-2025-05-14" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4-20250514",
    "max_tokens": 2048,
    "messages": [{"role": "user", "content": "..."}]
  }'

The SDKs expose the same thing as a first-class argument rather than a raw header, and using it is preferable because it also switches on the beta-typed request and response models:

# Python
message = client.beta.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=2048,
    betas=["interleaved-thinking-2025-05-14"],
    messages=[{"role": "user", "content": "..."}],
)

// TypeScript
const message = await client.beta.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 2048,
  betas: ["interleaved-thinking-2025-05-14"],
  messages: [{ role: "user", content: "..." }],
});

The dated suffix is part of the identifier. It is not a “since” date you can drop or bump: the value is matched literally, so interleaved-thinking without the date is simply an unrecognised beta name.

What breaks when the header is missing

There is no single “missing beta” error, and knowing which of the three you are looking at tells you what to fix.

  • Unknown parameter, HTTP 400. When the beta adds a request field, sending that field without the header fails schema validation. The message names the offending path, in the shape of messages.0.content.0: Extra inputs are not permitted or a complaint about an unexpected top-level property. This is the loud, easy case.
  • Silent fall-back to default behaviour. When the beta changes how something is encoded rather than adding a field — token-efficient tool use is the clearest example — the request is perfectly valid without the header and simply behaves the old way. Nothing errors. You discover it from a token count that did not change, which is why a beta whose benefit is a metric needs the metric checked and not just the code shipped.
  • Model does not support this beta. Sending a valid beta name to a model that does not implement it is its own failure. Betas are frequently scoped to specific model generations, and a header that worked on last quarter’s model can be inert or rejected on this quarter’s.

There is also a fourth case that is not an error at all: an unknown beta name is generally ignored rather than rejected, which is convenient for forward compatibility and inconvenient for typos. A misspelled beta name behaves exactly like no beta name.

The diagnostic order that works, when a beta appears not to be doing anything, is: confirm the header reached the wire rather than being dropped by a proxy or an SDK version too old to know the argument; confirm the name is character-for-character what the documentation gives, date included; confirm the model ID is one the feature supports; and only then look at the request body. Three of those four are outside the JSON, which is why this failure feels harder to debug than it is.

A related trap in shared code: beta headers configured once on a client object apply to every request that client makes. That is convenient until one route needs a beta that changes response shape and another route’s parser was written before it. Set betas per request, or keep separate clients, rather than putting a global default on a client several teams import.

Beta names, their model scopes and their expiry are documented per feature. Anthropic’s beta headers reference is the list to check before assuming a header from a six-month-old blog post is still live.

Why a header and not a version bump

The design is worth understanding because it explains the constraints you inherit from it. The alternatives an API vendor has for shipping something unfinished are a new API version, a new endpoint, or an opt-in flag. A version bump forces every caller to make a decision about every change at once, which is why anthropic-version has stayed at 2023-06-01 for years — it is reserved for changes that genuinely break the contract. A new endpoint fragments the surface and leaves you migrating call sites later.

A per-request header does neither. It is opt-in at the finest possible granularity: one route in your application can use a beta while every other route on the same client and the same API key does not. That matters more than it sounds. It means you can enable a beta for 5% of traffic, compare, and roll back by deleting a string — no separate client, no separate deployment, no version pinning exercise.

The cost of that design is the thing this page keeps returning to: headers are not in the request body. Anything that logs, replays, caches or reproduces requests by serialising the JSON loses them. A curl command reconstructed from a request log will behave differently from the request it was reconstructed from, and nothing will point at the reason. Whatever your request-logging layer is, make sure anthropic-beta is one of the fields it keeps.

Betas graduate, and then they disappear

The lifecycle has three stages and each has an operational consequence.

While a feature is in beta, the header is required and the shape can change between dated versions without notice — that is the point of the date in the name. When it reaches general availability, the header becomes unnecessary but is usually still accepted, so nothing breaks on the day it graduates. Prompt caching and message batches both went through this: their beta headers were required, then optional, then vestigial. Eventually the name can be retired, and the request that included it either errors or ignores it, depending on the feature.

Two habits follow from that. Keep beta names in configuration rather than scattered through call sites, because removing one after graduation should be a single edit. And log the header alongside the model ID on every request, because when a response shape changes for no apparent reason, the beta list is the first thing you want to be able to diff, and it is the one part of the request that is not in the body.