Back to guides
Troubleshooting·September 23, 2026·6 min read

Claude API 400 “Invalid `signature` in `thinking` block” — what broke the signature, and how to recover

The API checked the signature on a thinking block you sent back and it did not verify: the signature was truncated, altered or sent back empty, or the block was never signed by Claude — or, on Claude Fable 5.1 and Claude Opus 5.5, something earlier in the conversation changed. Resending the same history fails the same way every time. Find what broke it, then remove the thinking blocks from that conversation once and carry on — you lose the model's earlier reasoning, not the conversation.

Last reviewed on .

The API checked the signature on a thinking block you sent back and it did not verify: the signature was truncated, altered or sent back empty, or the block was never signed by Claude — or, on Claude Fable 5.1 and Claude Opus 5.5, something earlier in the conversation changed. Resending the same history fails the same way every time. Find what broke it, then remove the thinking blocks from that conversation once and carry on — you lose the model's earlier reasoning, not the conversation.

The error

response (HTTP 400)
// Through Kunavo: the upstream message as it reaches you,
// in the envelope Kunavo returns (type api_error, no request_id field):
{"type":"error","error":{"type":"api_error","message":"messages.1.content.0: Invalid `signature` in `thinking` block (request id: …)"}}
// (the message path, any masking of it and the appended request id vary by upstream)

// From Anthropic's API directly:
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages.1.content.0: Invalid `signature` in `thinking` block"
  },
  "request_id": "req_011C..."
}

// messages.{i}.content.{j}: i = position in messages[], j = block index. Both vary.
// An upstream can mask that path (***.***) and append its own request id, as above.
// Claude Code prints the body after "API Error: 400".
// On Claude Fable 5.1 and Claude Opus 5.5 the message can continue:
//   "... The block is bound to a different conversation. Remove the block, or set
//    `thinking.block_binding.prefix_mismatch_behavior` to "drop_block"."

Causes and fixes at a glance

CauseFix
The signature was truncated, emptied or edited before it came backStore and replay each block exactly as returned. Let the SDK assemble streamed turns so the signature_delta is not lost.
The block was never signed by ClaudeA non-Claude model behind an Anthropic-compatible URL, or a proxy that writes its own signatures. Send those turns back as text and tool_use only.
You switched base URL, account or login mid-conversationClaude Code 2.1.152+ strips stale signatures after a model or login switch. In your own code, strip thinking once if the first request after a switch fails.
“The block is bound to a different conversation” (Fable 5.1, Opus 5.5)The system prompt, tools or an earlier message changed. Keep the history append-only, or opt into drop_block (needs a beta header).
A proxy or gateway rewrites history on the way throughIts rewrites count as your edits. Reproduce against the API directly to rule it in or out.

Read which check failed

The wording tells you. A message that stops at “Invalid `signature` in `thinking` block” means the signature itself did not verify: Anthropic lists truncated, altered or sent back empty as the causes (https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting, as of September 2026), and its preserved-thinking page calls it a tampered or undecryptable signature that always returns a 400. A path masked as ***.***.content.0 or a request id appended by a gateway doesn't change that; what matters is whether a sentence about the conversation follows. On Claude Fable 5.1 and Claude Opus 5.5 the same words can continue with “The block is bound to a different conversation” — a different check, covered in the last step. A third message, “blocks in the latest assistant message cannot be modified”, means the newest assistant turn was edited, filtered, reordered or rebuilt; edited thinking text produces that error, not a signature error. Retrying the same body clears none of them.

three thinking-block 400s
Invalid `signature` in `thinking` block
  -> the signature did not verify: truncated, altered, empty, or not Claude's

Invalid `signature` in `thinking` block. The block is bound to a different conversation. ...
  -> Fable 5.1 / Opus 5.5: system, tools or an earlier message changed after the block was made

`thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified
  -> the newest assistant turn was edited, filtered, reordered or rebuilt before it was sent back

Send assistant turns back exactly as returned

Each thinking block carries a signature — an encrypted copy of the full reasoning — and the API uses it to verify that the block was generated by Claude (https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-encryption). Append the response's content list untouched: thinking, redacted_thinking and tool_use blocks, including thinking blocks whose text is empty, which is the default display on newer models. When streaming, the signature arrives in a single signature_delta just before the block closes, so a hand-rolled accumulator that misses it stores an empty signature, and a block sent back with an empty signature fails; Anthropic's advice is to let the SDK assemble the message. JSON key order and whitespace don't matter — the values do.

replay_verbatim.py
import anthropic

client = anthropic.Anthropic(base_url="https://api.kunavo.com", api_key="sk-kn-...")

tools = [{
    "name": "get_weather",
    "description": "Current weather for a city.",
    "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
    },
}]
messages = [{"role": "user", "content": "What's the weather in Paris?"}]

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "adaptive"},
    tools=tools,
    messages=messages,
) as stream:
    final = stream.get_final_message()   # signature_delta already applied

# Append the content list untouched: thinking, redacted_thinking, tool_use.
messages.append({"role": "assistant", "content": final.content})

# Not this: a store that keeps the text but not the signature replays
# {"type": "thinking", "thinking": "...", "signature": ""}  -> this 400.

Keep other backends' thinking out of Claude's history

Per Anthropic's docs, switching between Claude models on its own API is not supposed to trigger this: it asks you to keep sending the blocks when you switch, drops the ones the new model can't read without an error, and documents signatures as portable between the Claude API, Amazon Bedrock and Google Cloud (https://platform.claude.com/docs/en/build-with-claude/thinking, as of September 2026). What it cannot verify is a block Claude never signed. The public reports involve history that passed through another backend: a Claude Code session that ran on a GLM backend and then returned to Anthropic (github.com/anthropics/claude-code/issues/21726), Gemini turns a proxy dressed up as Claude thinking blocks with signatures of its own (github.com/router-for-me/CLIProxyAPI/issues/1584), a Claude Code session switched to another key mid-session and back (github.com/lbjlaq/Antigravity-Manager/issues/388). For turns a non-Claude model produced, Anthropic's advice is to send that model's output back as text and tool_use content only.

foreign_turn.py
def as_foreign_turn(content: list[dict]) -> list[dict]:
    """A turn a non-Claude model produced: keep what it said and did,
    never its thinking blocks, which Claude cannot verify."""
    return [b for b in content if b["type"] in ("text", "tool_use")]

Recover a conversation that already fails

Remove the thinking and redacted_thinking blocks from the stored history once — all of them is simplest; for the “bound to a different conversation” variant Anthropic's stated minimum is the named block and every one after it — keep every other block where it is, save that history, and continue. Anthropic gives this as the recovery for a saved session that can no longer be replayed (https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#faq); when a signature doesn't verify, the only other way out is replaying the block exactly as it was returned, if you still have it. Claude Code strips earlier thinking itself when the API rejects a signature. Once you have removed the blocks and moved on, don't put them back: on Fable 5.1 a block that was removed and put back invalidates the thinking produced while it was gone. The model answers without its earlier reasoning, and new thinking is valid from there. In Claude Code, 2.1.152 (May 27, 2026, https://code.claude.com/docs/en/changelog) strips stale signatures after a model or login switch, and its gateway guide says it retries a signature rejection without the earlier thinking blocks — but that retry matches on the upstream's error wording, and a gateway that wraps errors in its own envelope can break it (https://code.claude.com/docs/en/llm-gateway-protocol#automatic-retry-and-error-forwarding).

strip_thinking.py
THINKING = {"thinking", "redacted_thinking"}

def block_type(b) -> str:
    return b["type"] if isinstance(b, dict) else b.type   # dicts or SDK objects

def strip_thinking(messages: list[dict]) -> list[dict]:
    """One-time recovery: drop every thinking block, keep everything else."""
    out = []
    for m in messages:
        content = m["content"]
        if m["role"] == "assistant" and isinstance(content, list):
            kept = [b for b in content if block_type(b) not in THINKING]
            content = kept or [{"type": "text", "text": "(no visible reply)"}]  # keep the turn non-empty
        out.append({**m, "content": content})
    return out

messages = strip_thinking(messages)   # save this version; never re-add the blocks

On Fable 5.1, keep the prefix fixed — or opt into drop_block

“Bound to a different conversation” is the preserved-thinking check: on Claude Fable 5.1 and Claude Opus 5.5 a replayed block is valid only while the system prompt, the tools and every earlier message are unchanged. Anthropic enforces it by default for accounts created on or after August 31, 2026 (https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#enforcement); behind a gateway that account is not yours, so assume it is on. Keep system and tools fixed for the session and append rather than edit. To keep requests succeeding while you find the edit, send the thinking-binding-controls-2026-08-01 beta header with prefix_mismatch_behavior set to drop_block; without that header the field itself is rejected with “block_binding: Extra inputs are not permitted” (https://platform.claude.com/docs/en/api/errors).

drop_block.sh
# Anthropic's API directly. The field needs the beta header, which Kunavo does not forward.
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: thinking-binding-controls-2026-08-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-fable-5-1",
    "max_tokens": 16000,
    "thinking": {
      "type": "adaptive",
      "block_binding": {"prefix_mismatch_behavior": "drop_block"}
    },
    "messages": [{"role": "user", "content": "..."}]
  }'

If you’re calling through Kunavo

Kunavo passes thinking and redacted_thinking blocks through /v1/messages as sent, signature and data included — the only request changes are the model id and, on models that reject them, dropping temperature, top_p and top_k — and returns the response body, streamed or not, as the upstream sent it. As of September 2026 each Claude model is served through one upstream channel with no fallback, so Kunavo's routing does not move a conversation between providers, and a 400 is never retried elsewhere. We have not tested moving a conversation between Anthropic direct and Kunavo, so expect the first request after such a switch to need its thinking stripped. Kunavo does not forward anthropic-beta, so drop_block can't be turned on through it. The rejection arrives as HTTP 400 typed api_error, without a request_id field, carrying the upstream's message text — which can mask the messages.N path and end with the upstream's own request id — so match on the status and the words “Invalid `signature` in `thinking` block”. Claude Code's automatic strip-and-retry keys on that wording, and Anthropic's gateway guide says a gateway's own error envelope can break it; we have not tested it through Kunavo, so if a session fails on every turn, start a new one. Failed requests are not billed. What the native endpoint passes through untouched is listed in the Messages API reference.

FAQ

What does “Invalid `signature` in `thinking` block” mean?

The API could not verify a thinking block you sent back. Every thinking block carries a signature — an encrypted copy of Claude's reasoning — and the check fails when that signature was truncated, altered or sent back empty, or when the block was never signed by Claude. It is a 400, not a transient error: the same request fails every time.

Do thinking block signatures expire?

Anthropic's documentation doesn't mention an expiry. On the anthropic-sdk-python tracker (issue #1598, August 2026), a reply from an account GitHub marks as a contributor says they don't, and that the check fails when the block reaching the API differs from the one that was returned — an issue comment, not documentation. If a saved session that worked before now fails, look at what could have changed the stored blocks or the route they took: your storage layer, a proxy, or a switch of backend.

Can I just delete the thinking blocks and continue?

Yes. Anthropic gives it as the recovery for a saved session that can't be replayed, and Claude Code strips earlier thinking itself when a signature is rejected. Remove the thinking and redacted_thinking blocks — all of them is simplest — keep the other blocks, and retry once. The model loses its earlier reasoning, not the conversation; outside tool use, Anthropic's docs allow leaving earlier turns' thinking out anyway.

Why does it happen after switching models or providers?

Anthropic's docs say a switch between Claude models on its API drops the blocks the new model can't read, without an error, and that signatures work across the Claude API, Amazon Bedrock and Google Cloud. Claude Code still had to fix sessions stuck on stale signatures after a model or login switch (2.1.152), and the public reports involve history that passed through something Claude can't verify — a non-Claude model behind the same base URL, a proxy that writes its own signatures, or a client that lost them. Strip the thinking blocks once after the switch.

Does Claude Code fix this automatically?

Recent versions try to. Since 2.1.152 it strips stale signatures after a model or login switch, and it retries a signature rejection without the earlier thinking blocks. That retry matches on the upstream's error wording, and Anthropic's gateway guide says a gateway that wraps errors in its own envelope can break it. Run claude update first; if one session still fails on every turn, start a new session.

Related guides

More error semantics live in the error reference; getting a key takes a minute via sign up and the authentication docs.