# Claude Message Threads API guide

> Message threads in the Claude API: the beta header, create and continue requests, the fields you must resend, expiry and the 404 replay path.

- Canonical HTML: https://omniakey.com/blog/claude-message-threads-api-guide
- Markdown: https://omniakey.com/blog/claude-message-threads-api-guide.md
- Published: 2026-11-04 (updated 2026-11-04)
- Author: Kiki, COO at OmniaKey
- Language: en
- Category: API setup
- Reading time: 9 min
- All posts: https://omniakey.com/blog/llms.txt

---

The Claude message threads API lets you send a long conversation once, then send only the new messages on every later turn. Add `"thread": {"type": "create"}` to the first Messages request plus the `message-threads-2026-08-12` beta header, and the API keeps the conversation in memory so the next request uploads less (Anthropic's Message threads page, checked November 4, 2026).

It is a beta on every model; 17 model IDs can use it, including [Claude Opus 5.5](https://omniakey.com/claude-opus-5-5), which the code below calls. The beta header goes on every request of a thread.

## What a thread saves

Without a thread, every turn carries the whole transcript: the system prompt, the tool definitions, every earlier message and every tool result. A thread stores that conversation, including Claude's reply, under the response `id`, and a continue request sends only what happened since (Message threads page, checked November 4, 2026). Less to upload on every turn.

Prompt caching helps the same conversation in another way. A cache hit makes repeated input cheaper — on Opus 5.5, $0.20 per million tokens against $4 for fresh input (Anthropic's pricing page, checked November 4, 2026) — but the request still carries that input. A thread cuts what you send; caching cuts the price of what repeats. Long sessions want both, and [reducing Claude Code token usage](https://omniakey.com/blog/reduce-claude-code-token-usage) covers the context side.

Threads change neither pricing nor rate limits (Message threads page, checked November 4, 2026). Current rates are on [pricing](https://omniakey.com/pricing).

Threads leave caching to you. Send a top-level `cache_control` on every request and the API applies it to the last cacheable block of that request's `messages`. The four-breakpoint limit is a platform-wide prompt-caching rule, and automatic caching uses one of the four slots (Anthropic's prompt caching page, checked November 4, 2026). Spend them like this:

1. the top-level `cache_control`, one slot on every request of the thread;
2. one on `system`, sent with the create request;
3. one on `tools`, also on create;
4. one on the newest message.

Breakpoints on earlier stored messages sit outside that budget, since a continue request sends only new messages. Once sent, a breakpoint stays where it is. When you replay a conversation, strip the breakpoints from the earlier messages and keep one on the last block (Message threads page, checked November 4, 2026).

## Create the first turn, then continue

Three steps:

1. Put your Anthropic key in `ANTHROPIC_API_KEY`.
2. Send the full conversation, with `system` and `tools` if you use them, plus `"thread": {"type": "create"}`.
3. Send only the messages since the last response, plus `"thread": {"type": "continue", "previous_message_id": "<previous id>"}`, and leave `system` and `tools` out.

Every request carries the beta header and `anthropic-version: 2026-10-21`, the current version (Anthropic's versioning page, checked November 4, 2026).

```bash
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -H "anthropic-beta: message-threads-2026-08-12" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 1024,
    "system": "You are a careful backend reviewer.",
    "messages": [
      {"role": "user", "content": "Read this design and list the risks."}
    ],
    "thread": {"type": "create"}
  }'
```

Save the `id` from that response. The next request sends only the new message:

```bash
PREVIOUS_ID="<id from the previous response>"

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -H "anthropic-beta: message-threads-2026-08-12" \
  -H "content-type: application/json" \
  -d @- <<JSON
{
  "model": "claude-opus-5-5",
  "max_tokens": 1024,
  "messages": [
    {"role": "user", "content": "Turn those risks into a checklist."}
  ],
  "thread": {"type": "continue", "previous_message_id": "$PREVIOUS_ID"}
}
JSON
```

Keep the newest `id` from each reply: a thread is append-only, each response `id` can be continued once, and the next request must use the latest one (Message threads page, checked November 4, 2026).

If you need another way to get an API key, OmniaKey sells Claude API access with the same model IDs, so the request body stays the same. Threads are a beta on Anthropic's side, so before you depend on them in production, check that whatever proxy you use passes the `thread` field through unchanged.

The Python SDK keeps the ids for you. Build a helper with `client.beta.messages.thread(...)`, which only prepares the call, then send each turn with `thread.send()`. The helper adds the beta header, creates on the first turn, continues with only the new messages, and replays the full conversation once in a new create request when a continue returns `thread_not_found`, `thread_already_continued` or `thread_continue_in_flight` (Message threads page, checked November 4, 2026).

```python
import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

# The helper sends nothing yet; it holds the fields every turn reuses.
thread = client.beta.messages.thread(
    model="claude-opus-5-5",
    max_tokens=1024,
    system="You are a careful backend reviewer.",
)

first = thread.send({"role": "user", "content": "Read this design and list the risks."})
print(first.id, first.stop_reason)

second = thread.send({"role": "user", "content": "Turn those risks into a checklist."})
print(second.id)  # continue from this id next time
```

We did not run these requests against Anthropic's API; the request shapes come from Anthropic's Message threads page. For streams, `thread.stream(...)` plus `stream.get_final_message()` gives you the same flow, and `thread.previous_message_id` returns the id when you would rather track it yourself.

## What to resend and what never to resend

Send the fixed fields on every request with the same values, send `system` and `tools` only on the create request, and let the free fields change from turn to turn.

|Group|Fields|Rule|
|-|-|-|
|Create-only|`system`, `tools`|Create request only; sending either one on a continue request, even as `null`, is a 400.|
|Fixed|`model`, `tool_choice`, `thinking`, `mcp_servers` apart from each server's authorization token, `context_management`, `output_config`, the `skills` in `container`, the top-level `cache_control`, and the set of `anthropic-beta` values|Same value every time. A different value returns a 400 that names the field that differs.|
|Free|`max_tokens`, `stop_sequences`, `stream`, `metadata`, `service_tier`, and unnamed fields|Change them whenever you like.|

The 400 for a changed fixed field carries `thread_fingerprint_mismatch`, and the message lists the fields that differ. To change one of those values, start a new thread with the full conversation and the new value.

Omit `previous_message_id` and the continue returns a 400 whose body carries a message but no `error_code`; including `system` or `tools` does the same. A continue should carry exactly the messages since the last response.

`mcp_servers` is fixed, yet the server URLs and authorization tokens live only in your request, so send the list on every request that uses MCP; a token may change between requests. The container `id` lives only in your request too, so send `container` each time (Message threads page, checked November 4, 2026).

Server-side fallback is settled at create time: send `fallbacks` on every request of the thread or on none. The value may change between turns, but adding or dropping the field returns `thread_fingerprint_mismatch`.

## Expiry, replay and the 404 path

A thread expires 5 minutes after its last request, and the API deletes it no more than 1 hour after its last use. Each stored response restarts the 5 minutes. Threads are not eligible for zero data retention: the conversation, system prompt and tools sit in memory, encrypted, and are never written to durable storage (Message threads page, checked November 4, 2026).

Nothing in the response says whether the API stored a turn, on a create or a continue. You find out on the next continue request, so your application stays the source of truth and keeps its own copy of every message (Message threads page, checked November 4, 2026).

When a continue returns 404 `thread_not_found`, send the full conversation in a new create request and continue from that response's `id`. The API gives the same 404 for all of these:

- the thread expired;
- the previous turn was served and then dropped, for example a tool call cut off by `max_tokens`;
- a fallback model wrote the previous response;
- the previous response is still generating;
- the request came from another organization, workspace, user or workspace key;
- the new messages add content the stored conversation lacks, such as the first image in a conversation that had none.

Replaying the full conversation is always safe and repairs every one of those cases.

Streaming adds a rule: continue from the response `id` only after `message_stop`, because a continue sent earlier can return 404. If the stream of a continue request drops before `message_stop`, send the same continue request again, and replay if that returns `thread_already_continued` or `thread_continue_in_flight`. The SDK helper leaves a dropped stream for you to retry.

Three other cases have their own path:

- `pause_turn`: continue from that response's `id` with `"messages": []`. If that 404s, replay with the paused response included.
- `stop_reason: "refusal"`: start a new thread without the turn that caused it; the API may already hold the refused turn.
- A `fallback_message` entry in `usage.iterations`, or in the final `message_delta` on a stream, marks a fallback answer: replay.

## Errors

All five thread errors come back before the model runs: the request uses no tokens and the thread keeps its expiry time. `error.type` is `invalid_request_error` for the 400s and `not_found_error` for the 404 (Message threads page, checked November 4, 2026).

- **404, `thread_not_found`**: the API cannot continue this thread. Send the full conversation in a new
  create request.
- **400, `thread_fingerprint_mismatch`**: a fixed field changed from the value stored with the thread. Keep
  it identical, or start a new thread.
- **400, `thread_already_continued`**: that response id was already used once. Continue from the latest id,
  and replay if you never received it.
- **400, `thread_continue_in_flight`**: another continue from the same id is still running, usually your own
  earlier attempt. Let it finish or replay.
- **400, `thread_unsupported_request`**: the request uses a feature or an organization setting that threads
  do not support. `error.details.unsupported` names one of `context_management`, `cache_control`,
  `fallbacks`, `hipaa` or `inference_hooks`; that list can grow, so if a value looks unfamiliar, send the
  full conversation without `thread`.

Message Batches rejects the `thread` field (a batch item with it fails with `invalid_request_error`), and token counting and compaction return 400; so does context editing other than `clear_thinking_20251015` with `keep: "all"`. HIPAA-ready organizations and organizations with Inference hooks on are excluded too. Listing, reading and deleting a thread are all unavailable through the API.

## Frequently asked questions

### What is the Claude message threads API?

It is a beta Messages API feature that stores a conversation on Anthropic's side so later turns send only new messages. The beta header is `message-threads-2026-08-12`, and each request adds a `thread` object with `type` set to `create` or `continue`.

### Do message threads work with prompt caching?

Yes. A thread does not cache your prompt for you: send a top-level `cache_control` on every request and keep to four breakpoints. Breakpoints on stored messages sit outside that count; the top-level one and any on `system` or `tools` do count.

### Can I change the model in the middle of a thread?

Model changes need a new thread. `model` is a fixed field, so a different value returns 400 `thread_fingerprint_mismatch`; start a new thread with the full conversation and the new model.

### Why does a continue request return 400 with no error\_code?

The continue is missing `previous_message_id`, or it includes `system` or `tools` — even as `null`. Those 400s carry a message and skip `error_code`, so read the text.

### How long does a thread last, and what happens when it expires?

A thread expires 5 minutes after its last request and is deleted no more than 1 hour after its last use. After that a continue returns 404 `thread_not_found`; send the full conversation in a new create request and continue from its `id`.

## Sources checked

- [Anthropic: message threads](https://platform.claude.com/docs/en/build-with-claude/message-threads)
- [Anthropic: prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
- [Anthropic: Claude API pricing](https://platform.claude.com/docs/en/about-claude/pricing)
- [Anthropic: API versioning](https://platform.claude.com/docs/en/api/versioning)
