土曜の午後、iMac の前で Anthropic の認証ページを読み返していました。API キーの種類が整理し直されていて、「キーがワークスペースに紐づいていないと、リクエストにヘッダーを足さなければ 400 になる」という一節が目に留まりました。
読みながら気づいたのは、私の頭の中で「キーまわりの不具合は 401、直し方はキーの再発行」という図式が出来上がっていたことです。400 が返ってくる可能性を、それまで一度も想定していませんでした。
公式の Authentication ページに書かれている範囲を手がかりに、「キーは正しいのに失敗する」二つのパターンを切り分けてみます。なお、ここに載せるエラー文言は公式の記載から引いたものです。私自身の環境で組織設定を変えて再現した出力ではない点は、先にお伝えしておきます。
症状でまず二つに分けます
最初に見るのは、HTTP ステータスとエラーメッセージの冒頭だけです。ここを読まずにキーを作り直すと、原因が別のところにあるときに、新しいキーでも同じエラーが返ってきます。
| ステータス | メッセージの特徴 | 疑う場所 | キーは有効か |
|---|---|---|---|
| 400 | This API key is not scoped to a workspace … must include the anthropic-workspace-id header | リクエストのヘッダー | 有効 |
| 400 | anthropic-workspace-id header must be a valid workspace ID | ヘッダーの値の書式 | 有効 |
| 404 | Workspace … not found | ID の写し間違い、または権限 | 有効 |
| 401 | CORS requests are not allowed for this Organization | 呼び出し元がブラウザ | 有効 |
| 401 | 上記以外の authentication_error | キーそのもの(期限・削除・失効) | 無効の可能性 |
「有効」と書いた行は、キーを作り直しても直りません。ここが、再発行に走る前に立ち止まっていただきたい理由です。
パターン1: 400「ワークスペースに紐づいていない」
公式の説明では、ワークスペースを指定して作ったキーは、そのワークスペースでだけ動き、リクエストにワークスペース ID を書かなくても通ります。一方、ワークスペースを指定せずに作ったキーは、リクエストのたびに anthropic-workspace-id ヘッダーが必要です。ヘッダーを省くと、先ほどの表の一行目の 400 が返ります。
直し方は二通りあります。
一つ目は、ヘッダーを足す方法です。Python の SDK なら、クライアントを作るときに一度だけ指定できます。
from anthropic import Anthropic
# 解決したいこと: ワークスペース未指定のキーで、毎回ヘッダーを書かずに呼ぶ
client = Anthropic(
default_headers={"anthropic-workspace-id": "wrkspc_YOUR_WORKSPACE_ID"},
)
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=256,
messages=[{"role": "user", "content": "接続確認です。一言だけ返してください。"}],
)
print(message.content[0].text)ここでヘッダーをクライアント側に寄せているのは、呼び出しごとに足すと、一か所書き忘れた関数だけが 400 を返すからです。原因が「キー」でなく「書き忘れ」になると、ログを見ても気づきにくくなります。
二つ目は、キーを作り直す方法です。Console の API keys ページで「Create key」を押し、ワークスペースを指定して作ります。ただし、この方法は既存のキーを差し替える手間がかかります。単独の小さなスクリプトならヘッダーを足すほうが速く、複数の場所に配った鍵なら、作り直しの前に配布先の一覧を作っておくほうが安全です。
ID の確認先は、Console のワークスペース一覧の「ID」列です。公式の例では wrkspc_ で始まる形をしています。値が違うと 400(書式不正)、存在しない、または自分に権限がないと 404 になります。404 のメッセージは、どちらの場合も同じ文面になると書かれていました。写し間違いと権限不足を、メッセージだけでは区別できないということです。
パターン2: 401「CORS requests are not allowed for this Organization」
こちらは、キーが正しいのに 401 が返るという、いちばん紛らわしい症状です。
呼び出し元がブラウザ上の JavaScript のとき、リクエストには Origin ヘッダーが付きます。公式の説明では、ゼロデータ保持(ZDR)や Compliance API が有効な組織では、このヘッダーを持つリクエストは、直接呼び出し用のヘッダーを付けても拒否されます。Enterprise の親組織に紐づく Console 組織は、親の設定を引き継ぐため、自分の Console に Compliance API の項目が見えなくても対象になることがある、とも書かれていました。
もう一点、見落としやすい記述があります。自分のバックエンドが、ブラウザから届いたリクエストをそのまま転送していると、元の Origin ヘッダーも一緒に流れて、同じエラーになるのです。公式は「新しいリクエストを作り直して送る」ことを求めています。
要するに、ブラウザから Claude API を直接呼ぶ構成そのものが、組織によっては成立しません。直すのは、キーでも設定でもなく、構成です。
# 解決したいこと: ブラウザには鍵を渡さず、自分のサーバーだけが API を呼ぶ
# (Flask は一例です。構成の要点は「鍵はサーバーの環境変数にだけ置く」こと)
import os
from flask import Flask, request, jsonify
from anthropic import Anthropic
app = Flask(__name__)
client = Anthropic() # ANTHROPIC_API_KEY を環境変数から読む
@app.post("/api/ask")
def ask():
question = request.get_json(force=True).get("question", "")[:2000]
# ブラウザのリクエストを転送せず、ここで新しい API リクエストを作る
msg = client.messages.create(
model="claude-opus-5-5",
max_tokens=512,
messages=[{"role": "user", "content": question}],
)
return jsonify({"answer": msg.content[0].text})質問文を 2,000 字で切っているのは、画面側の入力がそのまま課金に直結しないようにするためです。
個人開発で自分の Web サイトを運用している私は、API キーをサーバー側の秘密情報にだけ置いています。画面に出るのは結果の文字列だけです。この線引きは、CORS の話以前に、キーがページのコードから読めてしまうという単純な理由で守っています。今回の 401 は、その線引きが組織の規則としても求められる場面がある、という確認になりました。
手元で動かせる判定スクリプト
エラーを受け取るたびに表を見比べるのは手間なので、ステータスとメッセージから原因の候補を返す関数を書いてみました。メッセージの文言は上の公式記載を前提にしています。文言が将来変わったときは、ここだけ直せば足ります。
import json
def triage(status: int, body: str) -> str:
"""Claude API のエラー応答を、鍵を作り直す前に切り分ける。"""
try:
msg = json.loads(body)["error"]["message"]
except (ValueError, KeyError, TypeError):
msg = body
if status == 400 and "not scoped to a workspace" in msg:
return "鍵は有効。anthropic-workspace-id ヘッダーが無い"
if status == 400 and "must be a valid workspace ID" in msg:
return "ヘッダーの値が ID の書式になっていない"
if status == 404 and msg.startswith("Workspace `"):
return "ID の写し間違い、または権限が無い"
if status == 401 and msg.startswith("CORS requests are not allowed"):
return "鍵は有効。ブラウザ直呼びが組織設定で禁止。サーバー側へ移す"
if status == 401:
return "鍵そのものを疑う: 期限切れ・削除・無効化・退会による失効"
return "別の原因: ステータスとメッセージを記録して次へ"手元で五つの例文を通したところ、期待どおりの候補が返りました。ただし、これは私が公式の文面を写して作った例文での確認です。実際の応答で試した結果ではありません。
最後の分岐に「期限切れ」を入れたのは、公式が、期限を過ぎたキーは 401 の authentication_error になり、再有効化できないと書いているからです。期限の設計そのものは、別の記事(APIキーに有効期限を付ける運用)に書いています。
切り分けの順番
私は次の順で見ることにしました。
- ステータスの確認。400 と 404 ならキーは有効なので、再発行は見送り
- 401 のときは、メッセージが
CORS requests are not allowedで始まるかの確認 - 始まる場合は、呼び出し元がブラウザか、転送されたブラウザのリクエストかの調査
- 始まらない場合は、期限・削除・無効化を Console の API keys ページで確認
この順なら、キーの再発行は最後の一手になります。
キーを疑うのは、ステータスとメッセージを読んだあとです。 今回の読み直しで、この順番だけは崩さないと決めました。
まずは、お手元のエラーログから 400 と 401 を一件ずつ拾い、上の表のどの行に当たるかを確かめるところから始めていただければと思います。