Claude API Webhook Integration Example (Working Setup)
Claude's API doesn't emit webhooks on its own — it's a request/response API, not an event system. So "Claude API webhook integration" almost always means one of two things: using an incoming webhook (from GitHub, Stripe, Slack, a form submission, etc.) to trigger a Claude call, or firing an outgoing webhook once Claude finishes generating a response, usually to notify another system or deliver a long-running result asynchronously.
This article covers both patterns with working code, plus the parts people usually get wrong: signature verification, timeouts on slow completions, and how to avoid blocking your webhook receiver while Claude is still generating. If you're building this on top of a managed API layer like SubToAPI, the same patterns apply — you're just swapping the Anthropic SDK call for an HTTPS request to /v1/messages.
The two webhook patterns
Incoming → Claude. Something happens elsewhere (a PR opens, a payment fails, a form is submitted), that system POSTs to your endpoint, and you call Claude to process the payload — summarize it, classify it, draft a reply.
Claude → Outgoing. You kick off a Claude call (often a long one — large context, tool use, multi-step agent work) and instead of making the caller wait on an open HTTP connection, you process it in the background and POST the result to a webhook URL when it's done.
Both need the same underlying piece: a server endpoint that can receive and validate HTTP POST requests. Below is a minimal but production-usable version of each.
Pattern 1: Incoming webhook triggers a Claude call
Example: GitHub sends a webhook on pull request events, you summarize the diff with Claude, and post the summary to a Slack webhook.
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json({ verify: captureRawBody }));
function captureRawBody(req, res, buf) {
req.rawBody = buf;
}
function verifyGithubSignature(req) {
const signature = req.headers["x-hub-signature-256"];
const hmac = crypto.createHmac("sha256", process.env.GITHUB_SECRET);
hmac.update(req.rawBody);
const expected = `sha256=${hmac.digest("hex")}`;
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
app.post("/webhooks/github", async (req, res) => {
if (!verifyGithubSignature(req)) return res.status(401).end();
// Respond immediately — GitHub expects a fast 200
res.status(202).end();
const diff = req.body.pull_request?.diff_url;
if (!diff) return;
const claudeRes = 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-5",
max_tokens: 512,
messages: [
{ role: "user", content: `Summarize this PR diff in 3 bullet points: ${diff}` },
],
}),
});
const data = await claudeRes.json();
const summary = data.content?.[0]?.text;
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: `PR summary:\n${summary}` }),
});
});
app.listen(3000);
The key detail: acknowledge the webhook before calling Claude. Most webhook senders (GitHub, Stripe, Slack) retry if they don't get a 2xx response within a few seconds, and a slow Claude completion will easily exceed that window. Respond with 202 immediately, then do the Claude call and the follow-up POST asynchronously.
Pattern 2: Notify a webhook when Claude finishes
For longer jobs — think agentic tool use with multiple round trips, or large-context analysis — don't make the original caller hold a connection open. Accept the job, return a job ID, and POST the result to a webhook URL when it's ready.
app.post("/jobs", async (req, res) => {
const jobId = crypto.randomUUID();
res.json({ jobId, status: "processing" });
// run this off the request lifecycle (queue, worker, background task)
processJob(jobId, req.body).catch(console.error);
});
async function processJob(jobId, payload) {
const result = 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-5",
max_tokens: 4096,
messages: payload.messages,
}),
}).then((r) => r.json());
await fetch(payload.callbackUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jobId, status: "completed", result }),
});
}
For anything beyond toy traffic, swap the fire-and-forget processJob call for a real queue (BullMQ, SQS, Cloud Tasks) so retries and failures are handled properly instead of dying with the process.
Streaming vs. webhooks
If you need live token-by-token output, that's a different integration — Server-Sent Events over an open connection, covered in /docs/streaming. Webhooks are for "notify me when this is done," not "stream this to me live." Don't try to make one pattern do the other's job; they solve different latency problems.
Signature verification matters more than it seems
Any endpoint that accepts POST requests from the public internet without verifying a signature is an open invitation for someone to trigger (and bill you for) Claude calls. Every webhook sender worth using signs its payloads — verify the signature before you do anything with the body, not after.
Where SubToAPI fits
SubToAPI doesn't send webhooks itself — it's a synchronous HTTPS API (/docs/messages), the same shape as Anthropic's. What it gives you for webhook-style integrations is a single API key per application (sub_live_...), usage metadata per request so you can track cost per webhook trigger, and tool use support (/docs/tools) if your webhook handler needs Claude to call functions as part of the job. You get a 5-minute setup via /docs/quickstart, a free trial at /signup, and plans starting at Solo €9 on /pricing.
Questions
Does Claude's API have native webhook support? No. It's request/response only. Webhook integration means building your own receiver that triggers Claude calls, or your own sender that posts results once a Claude job finishes — there's no built-in event push from Anthropic or from SubToAPI.
Should I respond to the webhook before or after calling Claude? Before. Most webhook senders retry if they don't get a fast 2xx, and Claude completions — especially with tool use or long context — can exceed those timeouts. Acknowledge immediately, process async.
How do I avoid duplicate Claude calls from webhook retries? Store the webhook's delivery ID or event ID and check it before processing. If the sender retries the same event (common with at-least-once delivery), skip it instead of calling Claude again.