How to use webhooks with the Anthropic API in 2026. Cover the Batch API callback flow, verifying webhook signatures, and a complete Express/Flask receiver pattern.
Anthropic's real-time messages.create endpoint is request/response — there is no webhook on individual completions. Webhooks come into play for async workloads: the Batch API, long-running jobs, and any pipeline where you want a push notification when work finishes rather than polling.
| Surface | Pattern | Webhook? |
|---|---|---|
| messages.create (real-time) | HTTP request/response, streaming or full | No — synchronous |
| Batch API | Submit batch, poll status OR receive webhook on completion | Yes |
| Files API uploads | Upload, then reference by file_id | No |
| Claude.ai / Console events | Org/billing events | No public webhook product in 2026 |
/v1/messages/batches with a webhook_url field on the request.batch_id.webhook_url with the batch metadata./v1/messages/batches/<id>/results to download the per-message outputs.The webhook does not contain the full results — it is a completion notification. You still pull the output file via the API. This is identical to the OpenAI Batch + S3 pattern.
import express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.ANTHROPIC_WEBHOOK_SECRET;
app.post("/anthropic-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("anthropic-signature");
const ts = req.header("anthropic-timestamp");
const payload = ts + "." + req.body.toString("utf8");
const expected = crypto
.createHmac("sha256", SECRET)
.update(payload)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
// 5-minute replay window
if (Math.abs(Date.now()/1000 - Number(ts)) > 300) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "batch.completed") {
enqueueBatchResultDownload(event.data.batch_id);
}
res.status(204).end();
}
);
Anthropic retries webhook delivery on non-2xx responses for up to 24 hours with exponential backoff. Your handler must be idempotent: dedupe by event.id in a small store (Redis SET with 7-day TTL is sufficient). If you 2xx but then crash before processing, you will not get a re-delivery — process inside the same transaction as the 2xx, or queue first and ack later.
Use ngrok or cloudflared tunnel to expose your local handler to Anthropic's outbound webhook traffic during development. Set webhook_url on the batch request to the public tunnel URL. Production should always use a stable public endpoint (DNS, not an ngrok URL).
The Batch API also supports polling. GET /v1/messages/batches/<id> returns processing_status (in_progress / completed / canceled / expired / failed). Poll every 30–60 seconds. Webhooks are strictly better for latency and infra cost; polling is fine for low-volume or batch-completion-doesn't-matter workloads.
For real-time completions, the stream=true response is the Anthropic equivalent of a webhook: each SSE event is a partial completion. For an architecture where Claude finishes asynchronously and pushes the final answer, the canonical pattern is: streaming response → your server collects tokens → your server fires its own webhook to a downstream consumer.
For background on cost differences between sync, batch, and streaming, see Batch vs Streaming: Cost.