Managed Agentsのwebhookで署名検証する実装手順
Claude Managed Agentsのwebhookエンドポイント登録・署名検証・配信保証の仕組みを、実装コードと自動無効化の条件まで含めて解説します。
Claude Managed Agentsのセッションは数分から数時間動き続けます。常時SSEストリームを張らずに主要な状態変化だけ受け取りたいなら、webhookが使えます。webhookイベントには対象オブジェクトの種別とIDしか含まれず、詳細はGETで取得し直す設計です。署名はAnthropic公式SDKのunwrap()ヘルパーで検証でき、配信は最大3回まで再試行されますが、失敗し続けたイベントは再送されずに失われます。
webhookとSSEイベントストリームの役割分担
Managed Agentsのリアルタイムなやり取りの大半はSSEイベントストリームで行われます。webhookはそれとは別の目的を持ち、セッションが待機状態に入った・エラーで終了した・予算上限に達したといった主要な状態変化だけを、ポーリングなしで受け取るための仕組みです。
webhookのペイロードは軽量です。イベントのtypeとidだけが含まれ、フルオブジェクトは含まれません。受信側は対象をGETで取得し直す必要があります。この設計には理由があります。再送があってもペイロードが常に最小限で、古いデータを配信してしまうリスクがありません。
対応するイベント種別を把握する
Managed Agentsのwebhookは、セッション・vault・agent・scheduled deployment・deployment run・environment・memory storeという7つのリソース系統にまたがるイベントを配信します。セッション系だけでも次のように幅があります。
| イベント名 | 発火タイミング |
|---|---|
session.status_run_started | 発火タイミング実行が始まった(状態がrunningに遷移するたび) |
session.status_idled | 発火タイミングツール権限の承認待ちや新規メッセージ待ちなど、入力待ちに入った |
session.budget_reached | 発火タイミング設定した予算に到達し一時停止した(同じ予算値では1回のみ発火し、予算を変更すると再度有効になる) |
session.status_rescheduled | 発火タイミング一時的なエラーが発生し自動的に再試行している |
session.status_terminated | 発火タイミング復旧不能なエラー、またはアーカイブによりセッションが終了した |
session.thread_created / session.thread_idled / session.thread_terminated | 発火タイミングマルチエージェント構成の子エージェントやadvisorの開始・入力待ち・終了 |
session.outcome_evaluation_ended | 発火タイミング1回分のアウトカム評価が完了した |
session.updated / session.deleted | 発火タイミングセッションのプロパティ変更、または完全削除 |
他にもvault(vault.created・vault_credential.refresh_failed等)、agent(agent.updatedは新バージョン公開時のみ発火し、バージョンを作らない更新では発火しません)、scheduled deployment(deployment.pausedは依頼による一時停止に加え、アーカイブ済みのサブエージェントや環境といった復旧不能なエラーでスケジュール実行が失敗した場合の自動一時停止も含みます。レートリミットなど復旧可能な失敗では一時停止しません)、deployment run、environment、memory storeの各系統がそれぞれ専用のイベントを持ちます。memory_store.deletedはメモリー本体や個々のバージョンの削除イベントを個別には出さず、ストア削除の1イベントだけがシグナルになる点は見落としやすいところです。
スケジュール実行とマルチエージェントの状態変化も拾える
scheduled deploymentsを使っている場合、webhookはデプロイメント本体の変化(deployment.paused・deployment.unpaused・deployment.archived)と、個々の実行の変化(deployment_run.started・deployment_run.succeeded・deployment_run.failed)を別系統で配信します。deployment_run系のイベントはスケジュール実行だけが発火し、手動実行では発火しません。deployment_run.succeededはセッションを作成できたことしか意味せず、セッション自体が成功したかどうかはsession_idを取得してセッションイベントを追う必要があります。
マルチエージェント構成を使っているセッションでは、コーディネーターが呼び出した子エージェントやadvisorの開始・待機・終了もsession.thread_created・session.thread_idled・session.thread_terminatedとして個別に届きます。子スレッドが作業を終えてidleになるのと、スレッドがterminatedになるのは別の状態です。作業完了とスレッド終了を同じイベントで判定しようとすると、コーディネーターが子エージェントを再利用する構成で状態を取り違えます。
vault関連では、vault_credential.refresh_failedを購読しておくと、mcp_oauthの認証情報でリフレッシュトークンが失効したり、OAuthサーバー側で復旧不能なエラーが起きたタイミングをポーリングなしで検知できます。認証情報のローテーション処理をこのイベントにフックしておけば、セッション側でmcp_authentication_failed_errorが出るより先に手を打てます。
エンドポイントを登録する
Consoleの「Manage > Webhooks」からエンドポイントを登録します。登録時に必要な要素は3つです。
| 要素 | 内容 |
|---|---|
| URL | 内容HTTPS・ポート443・パブリックに解決可能なホスト名が必須 |
| イベント種別 | 内容このエンドポイントが受け取るdata.typeの一覧(購読していない種別は届かない) |
| 署名シークレット | 内容whsec_で始まる32バイトの秘密鍵。作成時に一度だけ表示される |
署名シークレットは再表示されないため、作成直後に安全な場所へ保存します。エンドポイントが受け取るイベント種別は登録時に選べるため、たとえばセッション系のイベントだけを購読して他の系統は購読しない、といった絞り込みができます。購読していないイベント種別は届かない仕様なので、必要なイベントを漏らさず選んでおくことが重要です。
署名を検証する
すべての配信にはwebhook-id・webhook-timestamp・webhook-signatureの3つのヘッダーが付きます。公式SDKのunwrap()を使えば、署名検証とペイロードのパースを1ステップで済ませられます。署名が無効か、ペイロードが5分より古い場合は例外を投げます。
環境変数ANTHROPIC_WEBHOOK_SIGNING_KEYに、エンドポイント作成時に表示されたwhsec_シークレットを設定しておきます。
from flask import Flask, request
import anthropic
client = anthropic.Anthropic() # ANTHROPIC_WEBHOOK_SIGNING_KEYを読む
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
try:
event = client.beta.webhooks.unwrap(
request.get_data(as_text=True),
headers=dict(request.headers),
)
except Exception:
return "invalid signature", 400
if event.data.type == "session.status_idled":
print("session idled:", event.data.id)
return "", 200TypeScript(Express)で実装する場合の落とし穴は、ボディパーサーにexpress.json()ではなくexpress.raw()を使う点です。署名はraw bytesに対して計算されるため、JSONへパースしてから検証すると必ず失敗します。
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = client.beta.webhooks.unwrap(req.body.toString("utf8"), {
headers: req.headers as Record<string, string>
});
} catch {
return res.status(400).send("invalid signature");
}
// event.data.type で分岐する
res.sendStatus(200);
});イベントを処理してレスポンスを返す
data.typeで分岐し、IDを使って対象リソースを取得し直します。2xx系のレスポンスを返せば受信確認は完了です。
{
"type": "event",
"id": "whe_9d5c1f7e...",
"created_at": "2026-03-18T14:05:22Z",
"data": {
"type": "session.status_idled",
"id": "sesn_01XYZ...",
"organization_id": "8a3d2f1e-...",
"workspace_id": "c7b0e4d9-..."
}
}トップレベルのevent.idはイベントごとに一意ですが、配信ごとには一意ではありません。同じevent.idを2回受け取ったら、それは再送であり中身は同一です。冪等に処理するには、このevent.id(webhook-idヘッダーと同じ値)で重複排除します。実装としては、event.idにユニーク制約を張ったテーブルへ処理済みイベントを記録し、INSERTが重複違反で失敗したら処理をスキップする、という単純な仕組みで十分間に合います。
配信保証の落とし穴 — 順序もキューイングも保証されない
webhookを組み込むときに最も誤解されやすいのが、配信の保証範囲です。
- 順序は保証されない:
session.status_idledがsession.outcome_evaluation_endedより先に届くことがあり、.deletedイベントが同一リソースの.archivedイベントより先に届くこともあります。イベントの到着順ではなく、GETで取得したリソースの状態を正として実装します。 - 購読前のイベントは失われる: あるイベント種別に対して購読中のエンドポイントが1つも無いタイミングで発生したイベントは配信されず、あとから購読を追加しても遡って届きません。必要なイベント種別は使う前に購読しておく必要があります。
- 再送は最大3回だけ: 5秒から120秒のジッター付き指数バックオフで最大3回試行し、それでも失敗すると、そのイベントはキューに積まれず破棄されます。喪失を知らせるシグナルもありません。すべての遷移を漏れなく把握したい場合は、webhookだけに頼らずAPIでリソースを定期的に一覧・取得して突き合わせる必要があります。
- タイムスタンプは配信のものであってイベント発生時刻ではない:
webhook-timestampヘッダーは再送のたびに再生成される「配信試行の時計」です。イベントが実際に発生した時刻はペイロード内のcreated_atを使います。
エンドポイントが自動的に無効化される3つの条件
エンドポイントは次の3ケースでdisabledに切り替わり、disabled_reasonにその理由が入ります。
| 条件 | 挙動 |
|---|---|
3xxを返した | 挙動リダイレクトは追跡されず、初回の失敗で即座に無効化 |
| URLが非公開IPに解決される | 挙動接続を試みた時点で即座に無効化 |
| 継続的な配信失敗 | 挙動失敗が中断なく続いた期間で判定(累計回数ではない)。1回でも2xxが返れば計測はリセットされる |
いずれも復旧可能で、原因を解消したあとConsoleから再度有効化できます。無効化されていた間に発生したイベントは、再有効化後に再送されません。
アーカイブと削除は冪等だが、通知されるとは限らない
environment.updatedは変更されたフィールドが1つ以上あるときだけ発火し、実質的に何も変わらない更新(no-op)はイベントを出しません。environment.archivedもすでにアーカイブ済みの環境を再度アーカイブしようとした場合はイベントを出さず、vault.archivedのような他のリソースのarchivedイベントも同様の挙動です。「アーカイブAPIを呼んだのにwebhookが来ない」という状況は、実装のバグではなく状態が変わっていないことの正常なシグナルである場合が大半です。
アーカイブは連鎖することもあります。vaultをアーカイブすると、配下の各credentialに対しても個別にvault_credential.archivedが発火します。同様にdeploymentも、直接アーカイブした場合だけでなく、紐づくagentがアーカイブされた場合に連動してアーカイブされます。ただしagent自体が削除された場合は挙動が違い、deploymentはその場では消えず、次回のスケジュール実行のタイミングでアーカイブされます(スケジュールを持たないdeploymentは自動アーカイブの対象外です)。子リソースへの連鎖が起きるのか、単なる状態不変で発火しないだけなのかを取り違えると、「削除したはずのリソースがまだ生きている」ように見える誤診断につながります。
一方で、自己ホスト型サンドボックスの環境が持つ作業項目(work items)は、いかなる状態変化でもwebhookイベントを一切発行しません。自己ホスト環境のジョブ進行状況をリアルタイムに追いたい場合は、webhookではなくAPIでのポーリングか、セッションのSSEイベントストリーム側で状態を追う設計にする必要があります。「環境がwebhookを購読していれば全ての子リソースの変化も拾える」という前提を置くと、自己ホスト構成でだけ想定外の抜け漏れが生まれます。
まとめ
Managed Agentsのwebhookは、長時間セッションの主要な状態変化をポーリングなしで受け取るための補助チャネルであり、SSEストリームの完全な代替ではありません。署名検証はunwrap()に任せ、event.idで重複排除し、順序や配信保証を前提にしない設計にしておけば、大半の落とし穴は避けられます。ただし配信は最大3回で打ち切られ、失敗したイベントは復元できないため、すべての遷移を厳密に追う必要がある用途では、Managed AgentsのMCP接続で触れたSSEストリームや、APIでの定期的な状態確認と併用します。Managed Agents全体の設計はManaged Agentsの設計思想を参照してください。