← Blog

Claude API Webhook Event Handling: A Practical Guide

2026-09-27 · 5 min read · SubToAPI Team

What "Claude API webhooks" actually means

The Claude API itself does not emit webhooks. It's a request/response HTTP API: you send a prompt, you get a completion (or a stream of chunks) back on the same connection. There's no built-in mechanism for Anthropic to push events like message.completed or message.failed to a URL you register.

What people usually mean by "Claude API webhook event handling" is one of two real-world patterns:

  1. Incoming webhooks that trigger a Claude call. A third-party system (Stripe, GitHub, Slack, a form submission service) sends you an event, and your handler calls Claude to process, summarize, classify, or respond to that event.
  2. Outgoing webhooks you build yourself to notify other systems once a Claude job (often a long or batched one) finishes, so callers don't have to poll.

This guide covers both, plus the reliability and security details that matter once this is running in production.

Pattern 1: Webhook receiver that calls Claude

This is the most common setup: GitHub sends a webhook when an issue is opened, and you want Claude to draft a response or triage label.

The critical rule is respond to the webhook sender immediately, then process asynchronously. Most webhook providers time out after 5–15 seconds and will retry (or give up) if you don't return a 2xx response quickly. A Claude completion can easily take longer than that, especially with tool use or a large context window.

// Express example: receive webhook, ack fast, process later
app.post("/webhooks/github", async (req, res) => {
  const isValid = verifySignature(req);
  if (!isValid) return res.status(401).send("invalid signature");

  // Ack immediately
  res.status(202).send("accepted");

  // Process off the request/response cycle
  queue.add("handle-issue-event", { payload: req.body });
});

The worker that consumes the queue then does the actual Claude call:

async function handleIssueEvent(job) {
  const { payload } = job.data;

  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: 500,
      messages: [
        { role: "user", content: `Triage this GitHub issue: ${payload.issue.body}` },
      ],
    }),
  });

  const data = await response.json();
  await postCommentToGitHub(payload.issue.number, data.content[0].text);
}

Decoupling the webhook receipt from the Claude call with a queue (BullMQ, SQS, Cloud Tasks — anything durable) is what makes this reliable. Without it, a slow or failed Claude call means a dropped webhook and a source system that thinks delivery failed, even though your app already has the payload.

Pattern 2: Emitting your own webhook when Claude finishes

If you're running longer jobs — batch document processing, multi-step tool use, or anything where a client kicks off work and doesn't want to hold a connection open — you'll want to notify them when it's done instead of making them poll.

The shape is simple:

async function processAndNotify(jobId, prompt, callbackUrl) {
  try {
    const result = await callClaude(prompt);
    await notifyWebhook(callbackUrl, {
      event: "job.completed",
      job_id: jobId,
      output: result,
    });
  } catch (err) {
    await notifyWebhook(callbackUrl, {
      event: "job.failed",
      job_id: jobId,
      error: err.message,
    });
  }
}

async function notifyWebhook(url, body) {
  const signature = signPayload(body, process.env.WEBHOOK_SECRET);
  await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Signature": signature,
    },
    body: JSON.stringify(body),
  });
}

Sign every outgoing payload with an HMAC so the receiver can verify it actually came from you. Include a timestamp and reject requests older than a few minutes on the receiving end to prevent replay.

Retries, idempotency, and timeouts

A few things break most webhook + LLM integrations in practice:

Where SubToAPI fits

If you're building this kind of pipeline — webhook in, Claude call, response or notification out — you still need standard API infrastructure: scoped API keys per service or environment, usage visibility so you know which webhook source is generating the most Claude spend, and a stable HTTPS endpoint that behaves predictably under retries.

SubToAPI turns your existing Claude access into that kind of API: issue a sub_live_... key per integration (one for the GitHub bot, one for the Slack bot, one for internal batch jobs), track usage per key in the dashboard, and call /v1/messages the same way regardless of which webhook source triggered the request. Start with /signup, check /docs/quickstart for the first request, and see /docs/messages for the full request/response shape. Plans are listed at /pricing.

questions

Does the Claude API support native webhooks for events like message completion? No. Claude API calls are synchronous request/response (or streamed over one connection). If you need webhook-style notifications, you build them yourself by wrapping the Claude call in your own job handler and firing an HTTP callback when it finishes.

How do I avoid duplicate Claude calls when a webhook sender retries? Store the unique event ID from the incoming webhook and check it before processing. If you've already handled that ID, return a 2xx immediately without calling Claude again.

Can I stream Claude's response directly back through a webhook? Not through the original webhook connection — those expect a fast acknowledgment, not a held-open stream. Use the webhook as a trigger, process asynchronously, and stream results to your own client via SSE or websockets if needed.

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 →