How to Set Up a Claude API Webhook (Step by Step)
If you searched for "how to set up Claude API webhook," here's the direct answer: Anthropic's Claude API doesn't have native webhooks. It's a synchronous request/response API — you send a prompt, you get a completion back in the same HTTP call (or as a stream). There's no "notify me when it's done" event system built into the API itself.
What people actually mean when they ask this is usually one of two things: they want to call Claude from an incoming webhook (e.g., a Slack event, a form submission, a Stripe event) and then act on the result, or they want to trigger an outgoing webhook once a Claude completion finishes, so another system gets notified asynchronously. Both are completely doable — you just build the webhook layer yourself, around Claude's request/response calls. Below is the pattern that works reliably in production.
The core pattern: incoming event → Claude call → outgoing webhook
Think of it as three stages, with Claude sitting in the middle as a processing step, not as the thing emitting events:
- Receiver — an HTTP endpoint that accepts an incoming webhook (from Slack, GitHub, your own frontend, a queue, etc.)
- Processor — your server calls Claude synchronously or via streaming, builds the response
- Dispatcher — your server POSTs the result to a target URL (the "outgoing webhook")
This keeps Claude stateless and lets you treat the webhook logic — retries, signatures, idempotency — as your own responsibility, which is exactly where it should live.
Step 1: Build the receiver
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/incoming", async (req, res) => {
// Acknowledge immediately — don't block on Claude
res.status(202).json({ received: true });
const { event_id, prompt, callback_url } = req.body;
processWithClaude(event_id, prompt, callback_url);
});
app.listen(3000);
Always acknowledge the incoming webhook fast (sub-second). Most webhook senders (Slack, GitHub, Stripe) will retry or time out if you don't respond within a few seconds, and a Claude completion can easily take longer than that.
Step 2: Call Claude asynchronously
async function processWithClaude(eventId, prompt, callbackUrl) {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }]
})
});
const data = await response.json();
await dispatchWebhook(callbackUrl, eventId, data);
}
This example uses SubToAPI's /v1/messages endpoint, which mirrors Claude's Messages API — same request shape, same response fields — but gives you a stable sub_live_... application key instead of juggling raw Anthropic credentials across services. See /docs/messages for the full request/response reference.
Step 3: Dispatch the outgoing webhook
import crypto from "crypto";
async function dispatchWebhook(url, eventId, payload) {
const body = JSON.stringify({ event_id: eventId, result: payload });
const signature = crypto
.createHmac("sha256", process.env.WEBHOOK_SIGNING_SECRET)
.update(body)
.digest("hex");
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Signature": signature
},
body
});
}
Sign every outgoing payload with HMAC so the receiving system can verify it actually came from you. This is the same pattern Stripe, GitHub, and most mature webhook providers use — don't skip it just because you're the one building it.
Handling retries and failures
Outgoing webhook calls fail — the receiving server is down, DNS hiccups, timeouts. Treat your dispatcher like a queue consumer:
- Retry with exponential backoff (e.g., 1s, 5s, 30s, 2m, 10m)
- Cap retries (5–7 attempts is typical) and log terminal failures somewhere you'll actually see them
- Include an
event_idin every payload so the receiver can dedupe if a retry lands after a late success
async function dispatchWithRetry(url, body, signature, attempt = 1) {
try {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Signature": signature },
body
});
if (!res.ok) throw new Error(`Webhook target returned ${res.status}`);
} catch (err) {
if (attempt >= 6) {
console.error(`Webhook permanently failed: ${err.message}`);
return;
}
const delay = Math.min(2 ** attempt * 1000, 600000);
setTimeout(() => dispatchWithRetry(url, body, signature, attempt + 1), delay);
}
}
When streaming is a better fit than webhooks
If the consumer is a UI waiting for a response in real time, don't build a webhook round-trip at all — use streaming instead. Claude supports server-sent events for token-by-token output, which is lower latency and simpler than receiver/dispatcher logic for interactive use cases. SubToAPI exposes this the same way; see /docs/streaming for setup. Reserve the webhook pattern above for background jobs, batch processing, or cross-system notifications where nothing is sitting on the other end of an open connection.
Where tool use fits in
If your "webhook" is actually triggering a multi-step action — Claude deciding to call a function, fetch data, or hit another API before responding — that's tool use, not webhook plumbing. Build that logic with Claude's tool-calling support (/docs/tools) first, then wrap the result in the outgoing webhook once the full tool loop completes.
Why route this through a single API layer
Once you have incoming receivers, Claude calls, and outgoing dispatchers running across multiple services, you want one place to see usage, debug a stuck request, and manage keys per team or environment — rather than re-implementing auth and logging in every microservice that touches Claude. That's the practical reason to put a layer like SubToAPI between your webhook infrastructure and the model: one sub_live_... key per application, usage metadata per request, and team-level key management instead of shared raw credentials. Check /pricing for plan details or /docs/quickstart to get a key running in a few minutes.
questions
Does the Claude API support native webhooks? No. It's a synchronous HTTP API (with optional streaming). Any webhook behavior — incoming triggers or outgoing notifications — has to be built on your own server around the Claude request/response call.
What's the difference between using webhooks and streaming with Claude? Streaming is for real-time, open-connection delivery to a client that's actively waiting (e.g., a chat UI). Webhooks are for asynchronous notification to a system that isn't connected — background jobs, cross-service callbacks, or batch processing.
How do I secure an outgoing webhook built around Claude? Sign the payload with HMAC using a shared secret, send the signature in a header, and have the receiver verify it before trusting the body. Also include an idempotent event_id so retried deliveries don't get processed twice.