订阅 webhook
在重大事件发生时收到通知,无需轮询。
会话是长时间运行的交互。虽然大多数实时交互通过 SSE 事件流进行,但 webhook 会在重大状态变化时通知您。
Webhook 事件返回事件 type 和 id,而不是完整对象。当您收到 webhook 事件时,需要使用 GET 调用直接获取对象。这避免了在重试时传递过时数据,并使每次传递保持较小。
支持的事件类型
| 事件 | 触发条件 |
|---|---|
session.status_run_started | 代理执行已启动。每次会话状态转换为 running 时触发。 |
session.status_idled | 代理等待输入,例如工具权限批准或新的用户消息。 |
session.status_rescheduled | 发生了瞬态错误,会话正在自动重试。 |
session.status_terminated | 会话遇到终端错误。 |
session.thread_created | 新的多代理线程已打开,意味着协调器调用的附加代理正在开始工作。 |
session.thread_idled | 多代理交互中的代理正在等待输入。 |
session.thread_terminated | 多代理线程已归档。 |
session.outcome_evaluation_ended | 单次迭代的目标评估已完成。 |
| 事件 | 触发条件 |
|---|---|
vault.created | 保险库已成功创建。 |
vault.archived | 保险库已归档。还会为每个底层凭据发出 vault_credential.archived 事件。 |
vault.deleted | 保险库已删除。还会为每个底层凭据发出 vault_credential.deleted 事件。 |
vault_credential.created | 凭据已成功创建。 |
vault_credential.archived | 凭据已归档,直接归档或因保险库归档而导致。 |
vault_credential.deleted | 凭据已删除,直接删除或因保险库删除而导致。 |
vault_credential.refresh_failed | mcp_oauth 凭据无法刷新(无效的刷新令牌,或 OAuth 服务器的不可恢复错误)。 |
注册端点
访问 Console 中的 Manage > Webhooks。
Webhook 端点包括:
- **URL:**必须是端口 443 上的 HTTPS,具有可公开解析的主机名。
- **事件类型:**此端点接收的
data.type值列表。端点只接收其订阅的事件,加上测试事件(请参阅传递行为)。 - **签名密钥:**创建时生成的 32 字节
whsec_前缀密钥。仅显示一次,因此请安全存储以验证 webhook 传递。
验证签名
每次传递都携带 X-Webhook-Signature 头。使用 SDK 的 unwrap() 辅助函数一步验证签名并解析事件。如果签名无效或有效载荷超过五分钟,它会抛出异常。
将 ANTHROPIC_WEBHOOK_SIGNING_KEY 设置为端点创建时显示的 whsec_ 前缀密钥。
from flask import Flask, request
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
app = Flask(__name__)
@app.route("/webhook", methods=["POST"])
def webhook():
try:
# unwrap() raises if the signature is invalid or the payload is stale
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)
# handle other event types
return "", 200
import express from "express";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
const app = express();
// IMPORTANT: use express.raw(), not express.json(). The signature is computed over raw bytes.
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
// unwrap() throws if the signature is invalid or the payload is stale
event = client.beta.webhooks.unwrap(req.body.toString("utf8"), {
headers: req.headers as Record<string, string>
});
} catch {
return res.status(400).send("invalid signature");
}
switch (event.data.type) {
case "session.status_idled":
console.log("session idled:", event.data.id);
break;
// handle other event types
}
res.sendStatus(200);
});
using Anthropic;
var client = new AnthropicClient(); // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
var app = WebApplication.Create(args);
app.MapPost("/webhook", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
var headers = request.Headers.ToDictionary(header => header.Key, header => header.Value.ToString());
UnwrapWebhookEvent webhookEvent;
try
{
// Unwrap() throws if the signature is invalid or the payload is stale
webhookEvent = client.Beta.Webhooks.Unwrap(body, headers);
}
catch
{
return Results.BadRequest("invalid signature");
}
if (webhookEvent.Data.TryPickSessionStatusIdled(out var idled))
{
Console.WriteLine({{CONTENT}}quot;session idled: {idled.ID}");
}
// handle other event types
return Results.Ok();
});
package main
import (
"fmt"
"io"
"net/http"
"github.com/anthropics/anthropic-sdk-go"
)
var client = anthropic.NewClient() // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
func webhook(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "could not read body", http.StatusBadRequest)
return
}
// Unwrap returns an error if the signature is invalid or the payload is stale
event, err := client.Beta.Webhooks.Unwrap(body, r.Header)
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
switch event.Data.Type {
case "session.status_idled":
fmt.Println("session idled:", event.Data.ID)
// handle other event types
}
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhook", webhook)
}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.UnwrapWebhookParams;
import com.anthropic.core.http.Headers;
import com.sun.net.httpserver.HttpServer;
// reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
void main() throws Exception {
var server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("/webhook", exchange -> {
var body = new String(exchange.getRequestBody().readAllBytes());
var headers = Headers.builder();
exchange.getRequestHeaders().forEach(headers::put);
try {
// unwrap() throws if the signature is invalid or the payload is stale
var event = client.beta().webhooks().unwrap(
UnwrapWebhookParams.builder()
.body(body)
.headers(headers.build())
.build());
event.data().sessionStatusIdled().ifPresent(idled ->
IO.println("session idled: " + idled.id()));
// handle other event types
exchange.sendResponseHeaders(200, -1);
} catch (Exception _) {
exchange.sendResponseHeaders(400, -1);
}
exchange.close();
});
}
use Anthropic\Client;
use Anthropic\Core\Exceptions\WebhookException;
$client = new Client(); // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
$body = file_get_contents('php://input');
$headers = getallheaders();
try {
// unwrap() throws if the signature is invalid or the payload is stale
$event = $client->beta->webhooks->unwrap($body, headers: $headers);
} catch (WebhookException) {
http_response_code(400);
exit('invalid signature');
}
match ($event->data->type) {
'session.status_idled' => print "session idled: {$event->data->id}\n",
// handle other event types
default => null,
};
http_response_code(200);
require "sinatra"
require "anthropic"
client = Anthropic::Client.new # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
post "/webhook" do
headers = request.env
.select { |key, _| key.start_with?("HTTP_") }
.transform_keys { it.delete_prefix("HTTP_").downcase.tr("_", "-") }
begin
# unwrap raises if the signature is invalid or the payload is stale
event = client.beta.webhooks.unwrap(request.body.read, headers: headers)
rescue StandardError
halt 400, "invalid signature"
end
if event.data.type == "session.status_idled"
puts "session idled: #{event.data.id}"
end
# handle other event types
status 200
end
处理事件
解析请求体,根据 data.type 进行切换,并通过 ID 获取资源。返回任何 2xx 以确认。其他任何内容(包括 3xx)都被视为失败并触发重试。
每个事件有效载荷都有相同的结构,包括事件类型、标识符和对象创建时间的时间戳。
{
"type": "event",
"id": "event_01ABC...",
"created_at": "2026-03-18T14:05:22Z",
"data": {
"type": "session.status_idled",
"id": "sesn_01XYZ...",
"organization_id": "8a3d2f1e-...",
"workspace_id": "c7b0e4d9-..."
}
}
if event.data.type == "session.status_idled":
session = client.beta.sessions.retrieve(event.data.id)
notify_user(session)
return "", 204
if (event.data.type === "session.status_idled") {
const session = await client.beta.sessions.retrieve(event.data.id);
notifyUser(session);
}
res.sendStatus(204);
if (webhookEvent.Data.TryPickSessionStatusIdled(out var idled))
{
var session = await client.Beta.Sessions.Retrieve(idled.ID);
NotifyUser(session);
}
return Results.StatusCode(204);
if event.Data.Type == "session.status_idled" {
session, err := client.Beta.Sessions.Get(r.Context(), event.Data.ID, anthropic.BetaSessionGetParams{})
if err != nil {
panic(err)
}
notifyUser(session)
}
w.WriteHeader(http.StatusNoContent)
event.data().sessionStatusIdled().ifPresent(idled -> {
var session = client.beta().sessions().retrieve(idled.id());
notifyUser(session);
});
exchange.sendResponseHeaders(204, -1);
if ($event->data->type === 'session.status_idled') {
$session = $client->beta->sessions->retrieve($event->data->id);
notifyUser($session);
}
http_response_code(204);
if event.data.type == "session.status_idled"
session = client.beta.sessions.retrieve(event.data.id)
notify_user(session)
end
status 204
顶层 event.id 对每个事件是唯一的,而不是对每次传递是唯一的。如果您收到相同的 event.id 两次,这是重试,您可以丢弃它。
传递行为
- 不保证顺序。
session.status_idled可能在session.outcome_evaluation_ended之前到达,即使结果先产生。如果顺序很重要,请使用created_at时间戳进行排序。 - **重试:**Anthropic 至少重试一次。重试传递相同的
event.id。 - 不跟随重定向。
3xx被视为失败。如果您的端点移动了,请在 Console 中更新 URL。 - **自动禁用:**在大约连续 20 次失败传递后,端点会自动设置为
disabled并带有机器可读的disabled_reason,或者如果主机名解析为私有 IP 或端点返回重定向则立即禁用。解决问题后在 Console 中手动重新启用。