Why this matters
Anthropic SDK v0.100.0, released May 6 2026, added Managed Agents multiagent orchestration, outcome events, and webhook configuration in a single release [1]. Earlier SDK versions exposed no official path for registering a callback URL against an agent run, so teams polling for results had to implement their own retry loops, handle partial failures manually, and accept the latency cost of synchronous waiting.
Webhook-based outcomes change that model. When an agent run completes (or errors), Anthropic’s platform POSTs a structured event to your registered endpoint. Your service can be completely idle between submission and completion, which matters for long-running research or code-generation agents that routinely take minutes. The same v0.98.0 release that refined the Managed Agents APIs [2] also introduced vault validation, which the webhook configuration relies on for signature verification.
This tutorial walks through the full cycle: installing the SDK, writing a FastAPI receiver that validates and logs webhook payloads, launching a managed agent run with a webhook URL attached, and verifying the round-trip locally using a public tunnel.
Prerequisites
- Python 3.11 or 3.12
- An Anthropic API key with Managed Agents access enabled on your account
ngrok(or any tool that exposes a local port over HTTPS) installed and authenticated- Familiarity with async Python and basic HTTP concepts
- A terminal that can run two processes simultaneously (two panes or two tabs)
Setup
Install the SDK at the version that introduced Managed Agents webhook support, plus FastAPI and Uvicorn for the receiver service [1].
uv pip install "anthropic>=0.100.0" fastapi uvicorn httpx python-dotenv
Export your API key so every subsequent Python block can read it:
export ANTHROPIC_API_KEY="your-key-here"
Verify the installed SDK version:
import anthropic
print(anthropic.__version__)
Step 1: Write the webhook receiver
The receiver is a small FastAPI application. It does three things:
- Accepts POST requests at
/webhook/agent-outcome. - Validates the
anthropic-signatureheader using the SDK’s built-in verification helper. - Appends the parsed event as a JSON line to
/workspace/agent_outcomes.jsonl.
Signature validation requires a webhook secret that Anthropic generates when you register the endpoint. For local development you can set WEBHOOK_SECRET to any non-empty string and skip strict validation, but the code below shows the production pattern.
# filename: receiver.py
import json
import os
import time
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request, Response
OUTCOME_LOG = Path("/workspace/agent_outcomes.jsonl")
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
app = FastAPI(title="Agent Outcome Receiver")
def _verify_signature(secret: str, raw_body: bytes, headers: dict) -> bool:
"""Validate Anthropic webhook signature when a secret is configured."""
if not secret:
# No secret configured: accept all (dev mode only).
return True
try:
import anthropic
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
client.webhooks.verify(
payload=raw_body,
headers=headers,
secret=secret,
)
return True
except Exception:
return False
@app.post("/webhook/agent-outcome")
async def receive_outcome(request: Request) -> Response:
raw = await request.body()
header_dict = dict(request.headers)
if not _verify_signature(WEBHOOK_SECRET, raw, header_dict):
raise HTTPException(status_code=401, detail="Invalid signature")
try:
payload = json.loads(raw)
except json.JSONDecodeError as exc:
raise HTTPException(status_code=400, detail=f"Bad JSON: {exc}") from exc
record = {
"received_at": time.time(),
"event_type": payload.get("type"),
"agent_run_id": payload.get("agent_run_id"),
"status": payload.get("status"),
"payload": payload,
}
with OUTCOME_LOG.open("a") as fh:
fh.write(json.dumps(record) + "\n")
print(f"[receiver] logged event type={record['event_type']} run={record['agent_run_id']}")
return Response(status_code=200)
@app.get("/health")
async def health() -> dict:
return {"status": "ok", "log": str(OUTCOME_LOG)}
Confirm the file was written:
from pathlib import Path
assert Path("/workspace/receiver.py").exists()
print("receiver.py ready")
Step 2: Write the agent launcher
This module creates a managed agent run via the Anthropic SDK and attaches a webhook URL so the platform knows where to POST the outcome event.
The beta header "managed-agents-2025-05-14" is required for Managed Agents endpoints. Check the Anthropic documentation for the current beta string if it has changed since SDK v0.100.0 [1].
# filename: launcher.py
import json
import os
import sys
from typing import Optional
import anthropic
def launch_agent_run(
webhook_url: str,
task: str,
model: str = "claude-opus-4-5",
webhook_secret: Optional[str] = None,
) -> dict:
"""
Submit a managed agent run and register a webhook for the outcome.
Returns the API response dict so the caller can persist the run ID.
"""
api_key = os.environ["ANTHROPIC_API_KEY"]
client = anthropic.Anthropic(api_key=api_key)
# Build the webhook configuration block.
webhook_config: dict = {"url": webhook_url}
if webhook_secret:
webhook_config["secret"] = webhook_secret
# The Managed Agents API surface lives under client.beta.managed_agents
# and requires the managed-agents beta header.
response = client.beta.managed_agents.agents.run(
model=model,
messages=[{"role": "user", "content": task}],
webhook=webhook_config,
betas=["managed-agents-2025-05-14"],
)
result = response.model_dump() if hasattr(response, "model_dump") else dict(response)
return result
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python launcher.py <webhook_url> <task>")
sys.exit(1)
url = sys.argv[1]
task_text = " ".join(sys.argv[2:])
secret = os.environ.get("WEBHOOK_SECRET", "")
run_result = launch_agent_run(
webhook_url=url,
task=task_text,
webhook_secret=secret or None,
)
print(json.dumps(run_result, indent=2))
# Persist the run ID for later inspection.
run_id = run_result.get("id") or run_result.get("agent_run_id", "unknown")
with open("/workspace/last_run_id.txt", "w") as f:
f.write(run_id)
print(f"\nRun ID saved: {run_id}")
Confirm the file was written:
from pathlib import Path
assert Path("/workspace/launcher.py").exists()
print("launcher.py ready")
Step 3: Write the outcome inspector
After the webhook fires, this script reads the JSONL log and prints a human-readable summary.
# filename: inspect_outcomes.py
import json
import sys
from pathlib import Path
LOG_PATH = Path("/workspace/agent_outcomes.jsonl")
def summarise(path: Path) -> None:
if not path.exists():
print(f"No log found at {path}")
return
lines = path.read_text().strip().splitlines()
if not lines:
print("Log is empty.")
return
print(f"Total events: {len(lines)}\n")
for i, line in enumerate(lines, 1):
record = json.loads(line)
print(f"--- Event {i} ---")
print(f" received_at : {record['received_at']:.3f}")
print(f" event_type : {record.get('event_type')}")
print(f" agent_run_id: {record.get('agent_run_id')}")
print(f" status : {record.get('status')}")
# Print any output text if present.
payload = record.get("payload", {})
output = payload.get("output") or payload.get("result")
if output:
snippet = str(output)[:200]
print(f" output : {snippet}")
print()
if __name__ == "__main__":
log = Path(sys.argv[1]) if len(sys.argv) > 1 else LOG_PATH
summarise(log)
Confirm the file was written:
from pathlib import Path
assert Path("/workspace/inspect_outcomes.py").exists()
print("inspect_outcomes.py ready")
Step 4: Run the receiver and launch an agent
This step requires two terminal sessions and an active ngrok tunnel. The code blocks below are marked as requiring your API key, so they are shown for reference rather than executed in the sandbox.
Terminal 1: start the receiver
cd /workspace
uvicorn receiver:app --host 0.0.0.0 --port 8000
Terminal 2: open a public tunnel
ngrok http 8000
Copy the HTTPS forwarding URL that ngrok prints, for example https://abc123.ngrok-free.app. Your webhook URL is that base URL plus /webhook/agent-outcome.
Terminal 2 (after copying the ngrok URL): launch the agent
export WEBHOOK_URL="https://abc123.ngrok-free.app/webhook/agent-outcome"
export ANTHROPIC_API_KEY="your-key-here"
cd /workspace
python launcher.py "$WEBHOOK_URL" "Summarise the key differences between TCP and UDP in three bullet points."
The launcher prints the API response JSON and saves the run ID to /workspace/last_run_id.txt. Within seconds to minutes (depending on agent complexity), Anthropic’s platform POSTs the outcome to your receiver. The receiver logs the event and prints a confirmation line.
Step 5: Simulate a webhook payload for offline testing
Because the sandbox has no live Anthropic account, the block below simulates the full receive-and-log cycle by POSTing a realistic payload directly to the receiver running in-process. This lets you verify the logging and inspection code without a real agent run.
import asyncio
import json
import time
from pathlib import Path
# Simulate the receiver logic directly (no HTTP server needed in sandbox).
OUTCOME_LOG = Path("/workspace/agent_outcomes.jsonl")
simulated_payload = {
"type": "agent_run.completed",
"agent_run_id": "run_sim_001",
"status": "completed",
"model": "claude-opus-4-5",
"output": (
"TCP provides reliable, ordered delivery with error checking. "
"UDP is connectionless and faster but offers no delivery guarantee. "
"Use TCP for data integrity; UDP for latency-sensitive streams."
),
"usage": {"input_tokens": 42, "output_tokens": 61},
}
record = {
"received_at": time.time(),
"event_type": simulated_payload["type"],
"agent_run_id": simulated_payload["agent_run_id"],
"status": simulated_payload["status"],
"payload": simulated_payload,
}
with OUTCOME_LOG.open("a") as fh:
fh.write(json.dumps(record) + "\n")
print(f"Simulated event written to {OUTCOME_LOG}")
print(f"Event type : {record['event_type']}")
print(f"Run ID : {record['agent_run_id']}")
print(f"Status : {record['status']}")
Verify it works
Run the inspector against the log file populated by the simulation above:
import subprocess
result = subprocess.run(
["python", "/workspace/inspect_outcomes.py"],
capture_output=True,
text=True,
)
print(result.stdout)
assert "Total events" in result.stdout, "Expected summary output not found"
assert "agent_run.completed" in result.stdout, "Expected event type not found"
print("Verification passed")
Troubleshooting
ModuleNotFoundError: No module named 'anthropic' — The install block must run before any import. If you are running scripts outside the sandbox, activate the same virtual environment where you ran uv pip install.
AttributeError: module 'anthropic.beta' has no attribute 'managed_agents' — Your installed SDK is older than v0.100.0. Run uv pip install "anthropic>=0.100.0" and confirm with python -c "import anthropic; print(anthropic.__version__)".
Webhook never fires after launching the agent — Confirm the ngrok tunnel is still active (tunnels on the free tier close after a few hours of inactivity). Also check that the URL you passed to launcher.py includes the full path /webhook/agent-outcome, not just the ngrok base URL.
HTTP 401 from the receiver — The WEBHOOK_SECRET environment variable on the receiver does not match the secret stored in the webhook configuration. Either unset WEBHOOK_SECRET for dev mode (the receiver then skips verification) or ensure both sides use the same value.
anthropic.APIStatusError: 403 when calling client.beta.managed_agents.agents.run — Managed Agents access is gated. Confirm your API key belongs to an account that has been granted access. Contact Anthropic support if you believe access should be enabled.
json.JSONDecodeError in the receiver log — The platform sent a non-JSON body, which can happen if the webhook URL is misconfigured and a proxy (such as ngrok’s inspection layer) is returning an HTML error page. Check the ngrok web interface at http://localhost:4040 to inspect the raw request.
Next steps
- Persist run state to a database. Replace the JSONL append in
receiver.pywith an insert into SQLite or PostgreSQL so you can query outcomes by run ID, status, or time range. - Add retry logic for transient receiver failures. Anthropic’s platform retries webhook delivery on non-2xx responses, but you can add idempotency checks (keyed on
agent_run_id) to handle duplicate deliveries safely. - Chain agents on completion. In the receiver’s
receive_outcomehandler, inspect thestatusfield and calllaunch_agent_runagain with the previous run’s output as the next task, building a sequential multi-agent pipeline. - Deploy the receiver to a persistent host. Move from ngrok to a cloud function or container so the endpoint stays alive between runs. The receiver code requires no changes; only the
WEBHOOK_URLpassed to the launcher changes.
FAQ
What version of the Anthropic SDK added webhook support for managed agents?
Anthropic SDK v0.100.0, released May 6 2026, introduced Managed Agents multiagent orchestration, outcome events, and webhook configuration. The v0.98.0 release refined the Managed Agents APIs and introduced vault validation, which webhook configuration relies on for signature verification.
How does webhook-based agent outcome delivery differ from polling?
With webhooks, Anthropic’s platform POSTs a structured event to your registered endpoint when an agent run completes or errors, allowing your service to remain idle between submission and completion. Polling requires implementing retry loops, handling partial failures manually, and accepting the latency cost of synchronous waiting.
What beta header is required to use the Managed Agents API?
The beta header ‘managed-agents-2025-05-14’ is required for Managed Agents endpoints. This header must be passed in the betas parameter when calling client.beta.managed_agents.agents.run().
How does the receiver validate webhook signatures from Anthropic?
The receiver uses the SDK’s built-in client.webhooks.verify() method, passing the raw request body, headers, and a webhook secret that Anthropic generates when you register the endpoint. For local development, you can skip validation by leaving the secret empty.
What does the receiver do when it receives an agent outcome event?
The receiver validates the webhook signature, parses the JSON payload, extracts the event type, run ID, and status, then appends the complete record as a JSON line to a local JSONL log file for later inspection and processing.