← Blog

Claude API Webhook Integration Example (Working Setup)

2026-10-07 · 4 min read · SubToAPI Team

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →