← Blog

How to Set Up a Claude API Webhook (Step by Step)

2026-11-06 · 5 min read · SubToAPI Team

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:

  1. Receiver — an HTTP endpoint that accepts an incoming webhook (from Slack, GitHub, your own frontend, a queue, etc.)
  2. Processor — your server calls Claude synchronously or via streaming, builds the response
  3. 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:

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.

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 →