Subscribe to webhooks

Get notified when major events happen without polling.


Sessions are long-running interactions. While most real-time interactions happen through the SSE event stream, webhooks notify you of major state changes.

Webhook events return the event type and id, not the full object. When you receive a webhook event, you need to fetch the object directly with a GET call. This avoids delivering stale data on retries and keeps every delivery small.

Supported event types

EventTrigger
session.status_run_startedAgent execution kicked off. This triggers at every session status transition to running.
session.status_idledAgent awaiting input, for example a tool permission approval or a new user message.
session.status_rescheduledA transient error occurred and the session is retrying automatically.
session.status_terminatedThe session hit a terminal error.
session.thread_createdNew multiagent thread opened, meaning an additional agent called by the coordinator is kicking off work.
session.thread_idledAn agent in a multiagent interaction is waiting for input.
session.thread_terminatedA multiagent thread was archived.
session.outcome_evaluation_endedOutcome evaluation for a single iteration completed.
EventTrigger
vault.createdVault successfully created.
vault.archivedVault archived. A vault_credential.archived event is also emitted for each underlying credential.
vault.deletedVault deleted. A vault_credential.deleted event is also emitted for each underlying credential.
vault_credential.createdCredential successfully created.
vault_credential.archivedCredential archived, either directly or as a result of vault archival.
vault_credential.deletedCredential deleted, either directly or as a result of vault deletion.
vault_credential.refresh_failedA mcp_oauth credential cannot be refreshed (invalid refresh token, or irrecoverable error from the OAuth server).

Register an endpoint

Visit Manage > Webhooks in Console.

A webhook endpoint consists of:

  • URL: Must be HTTPS on port 443 with a publicly resolvable hostname.
  • Event types: The list of data.type values this endpoint receives. An endpoint only receives events it's subscribed to, plus test events (see Delivery behavior).
  • Signing secret: A 32-byte whsec_-prefixed secret generated at creation. It's shown only once, so store it securely to verify webhook deliveries.

Verify the signature

Every delivery carries an X-Webhook-Signature header. Use the SDK's unwrap() helper to verify the signature and parse the event in one step. It throws if the signature is invalid or the payload is more than five minutes old.

Set ANTHROPIC_WEBHOOK_SIGNING_KEY to the whsec_-prefixed secret shown at endpoint creation.

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

Handle an event

Parse the body, switch on data.type, and fetch the resource by ID. Return any 2xx to acknowledge. Anything else (including 3xx) counts as a failure and triggers a retry.

Every event payload has the same structure, including the event type, identifier, and timestamp of when the object was created.

{
  "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

The top-level event.id is unique per event, not per delivery. If you receive the same event.id twice, it's a retry and you can discard it.

Delivery behavior

  • Ordering is not guaranteed. session.status_idled may arrive before session.outcome_evaluation_ended even if the outcome was produced first. Use the created_at timestamp to sort if ordering matters.
  • Retries: Anthropic retries at least once. The retry delivers the same event.id.
  • Redirects are not followed. A 3xx is treated as a failure. If your endpoint moves, update the URL in Console.
  • Auto-disable: An endpoint is automatically set to disabled with a machine-readable disabled_reason after roughly 20 consecutive failed deliveries, or immediately if the hostname resolves to a private IP or the endpoint returns a redirect. Re-enable manually in Console after resolving the issue.