← Blog

Claude API Webhook Integration Guide for Developers

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

If you're looking for a webhook_url field in the Claude API request body, it doesn't exist. Anthropic's API is a synchronous request/response interface — you send a message, you get a completion back (or a stream of tokens). There's no built-in mechanism for Claude to call your server when something happens, and no dashboard setting to register a webhook endpoint.

That doesn't mean webhook-style integration is off the table. It means you build it yourself, around the API, using two well-established patterns: inbound webhooks that trigger a Claude call, and outbound webhooks that notify your systems once a Claude call finishes. Both are common in production setups — support ticket routing, Slack bots, CI pipelines, CMS content generation — and this guide walks through how to wire them up correctly, including the parts people usually get wrong (retries, idempotency, signature verification).

Why Claude's API isn't webhook-native

Claude API calls are typically fast enough (seconds, not minutes) that a synchronous HTTP request works fine for most use cases. Webhooks exist to solve a different problem: notifying a third party about an event that happens on someone else's schedule, often asynchronously and after a delay. Since a Claude completion is something you initiate and you wait for, the natural integration point is your own backend, not Anthropic's infrastructure.

The practical implication: any "webhook integration" with Claude is really about integrating webhooks from other systems (Stripe, GitHub, a CMS, a support desk) into a pipeline where Claude does the processing step.

Pattern 1: Inbound webhook triggers a Claude call

This is the most common setup. An external service posts an event to your endpoint, your server extracts the relevant payload, calls Claude, and does something with the result.

// Express endpoint receiving a support ticket webhook
app.post("/webhooks/ticket-created", async (req, res) => {
  // Respond immediately — don't make the sender wait on Claude
  res.status(200).send("ok");

  const { ticketId, subject, body } = req.body;

  const completion = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 500,
      messages: [
        {
          role: "user",
          content: `Classify and summarize this support ticket:\n\nSubject: ${subject}\n\n${body}`,
        },
      ],
    }),
  });

  const result = await completion.json();
  await saveTriageResult(ticketId, result);
});

Two things matter here: acknowledge the webhook fast (most providers retry if you don't return 2xx within a few seconds), and do the Claude call after you've responded, not before. If Claude is slow or rate-limited, you don't want the sender's retry logic firing on top of your own processing.

Pattern 2: Outbound webhook after Claude finishes

If the trigger for a Claude call isn't a webhook but something else — a queue job, a scheduled task, a user action in your app — you may still want to notify downstream systems once the completion is ready. This is your own webhook, and you control both sides.

async function processAndNotify(jobId, prompt, callbackUrl) {
  const response = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    }),
  });

  const data = await response.json();

  await fetch(callbackUrl, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-webhook-signature": signPayload(data),
    },
    body: JSON.stringify({ jobId, status: "completed", result: data }),
  });
}

Sign the payload with an HMAC secret shared with the receiving service so they can verify it actually came from you. Skipping this step is the single most common security gap in homegrown webhook systems.

Handling retries and idempotency

Webhook senders retry on timeouts and non-2xx responses, and your Claude call might occasionally fail (rate limits, transient errors, network blips). Two rules keep this from causing duplicate work:

app.post("/webhooks/ticket-created", async (req, res) => {
  const eventId = req.headers["x-event-id"];
  const alreadySeen = await eventStore.exists(eventId);
  if (alreadySeen) return res.status(200).send("duplicate, ignored");

  await eventStore.mark(eventId);
  await queue.push({ type: "triage", payload: req.body });
  res.status(200).send("queued");
});

Streaming inside a webhook-driven flow

Webhook payloads are single, discrete events, so streaming Claude's response token-by-token back to a webhook sender rarely makes sense — the receiving endpoint just wants the final result. If you need live progress (e.g., updating a UI while Claude generates), pair the webhook trigger with a WebSocket or Server-Sent Events connection to the client, and use Claude's streaming mode internally to populate it. See /docs/streaming for the streaming request format if you're building that layer.

Where SubToAPI fits

If you're already routing Claude calls through SubToAPI — because you want application-scoped sub_live_... keys, usage metadata per key, or team seats instead of sharing one raw Anthropic key across services — the webhook patterns above work exactly the same way. You call https://api.subtoapi.app/v1/messages with Authorization: Bearer $SUBTOAPI_KEY from inside your webhook handler or worker, same as you would the native API. The usage metadata in each response makes it straightforward to log cost per webhook event if you're billing triage or classification work back to a customer or team. Setup takes a few minutes — see /docs/quickstart — and every plan starting at the Solo tier includes streaming and tool use if your webhook flow needs either. Check /pricing for the tiers or /signup to get an API key.

Building it right the first time

FAQ

Does the Claude API support native webhooks for streaming or async jobs? No. Anthropic's API is request/response only. Any webhook behavior — triggering calls from events or notifying systems after completion — is built on your own backend around the API.

How do I avoid duplicate Claude calls when a webhook sender retries? Store an idempotency key (event ID, ticket ID, job ID) before calling Claude, and check it on every incoming request. If it's already processed, skip the call and return the cached result.

Can I use SubToAPI keys inside a webhook handler the same way as a direct Anthropic key? Yes. Swap the endpoint and auth header for https://api.subtoapi.app/v1/messages with Authorization: Bearer $SUBTOAPI_KEY; the request and response shapes match, so existing webhook logic doesn't need to change. See /docs/messages for the request format.

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 →