Skip to main content

How to Receive Webhooks in Claude Code Channels

Build a tiny MCP server that pulls verified webhooks into a running Claude Code session, so Claude can react to GitHub, Stripe, CI, and other events while you work.

Building webhooks?
Svix is the enterprise ready webhook receiver. With Svix, you can have a secure, reliable, and scalable webhook receiver in minutes. Give it a try!

TL;DR

  • A Claude Code Channel is a small MCP server that pushes notifications into a running Claude Code session.
  • To receive webhooks, build the channel server as a polling loop: it pulls verified events from a webhook gateway and emits one notifications/claude/channel per event.
  • Pulling (instead of accepting inbound HTTP) means no tunnel, no exposed port, and no signing-secret plumbing in the channel server itself.
  • Point your webhook provider at the gateway, start Claude Code with --dangerously-load-development-channels server:webhook, and Claude reacts to events as they arrive.
  • This guide uses Svix Ingest as the gateway because it handles signature verification for every major provider, durable storage, replay, and polling endpoints out of the box.

Introduction

Anthropic just shipped Claude Code Channels, a way to push external events directly into a running Claude Code session. Think CI failures, monitoring alerts, payment webhooks, GitHub notifications. Instead of Claude waiting for you to ask it something, it can now react to things happening in your systems while you're not even at the terminal.

The webhook use case is the most immediately practical application of Channels. The challenge is that webhook providers need a public URL to send events to, and your Claude Code session runs locally as a subprocess that can't easily expose one. Even if you tunnel a port out, you still need to verify provider signatures yourself, and you really don't want to lose events while you're restarting Claude or iterating on the server.

That's exactly what Svix Ingest is built for. This walkthrough wires it up: a small channel server that polls Svix for verified GitHub events and pushes them into Claude Code. Swap GitHub for any of the providers Svix Ingest supports out of the box and the rest of the steps stay the same.

What you'll need

Before you start, make sure you have:

  • Claude Code v2.1.80 or later. Channels are new and require a recent version. Check with claude --version.
  • A claude.ai login. Channels don't work with Console or API key authentication.
  • Node.js 18 or later. We use Node's built-in fetch and ES modules. Check with node --version.
  • A free Svix Ingest account. Sign up here; no credit card required.
  • A GitHub repo you can add a webhook to for the demo. Any repo works, including a throwaway one.

If you're on a Claude Team or Enterprise plan, your admin needs to enable channels first. Individual and Pro/Max users should be fine.

How Claude Code Channels work

A channel is an MCP server that Claude Code spawns as a subprocess. It communicates with Claude Code over stdio, the same way other MCP servers do. What makes it a channel is that it declares the claude/channel capability and emits notifications that the running Claude Code session listens for.

The flow for webhooks looks like this:

  1. An external service (GitHub, Stripe, your CI pipeline) POSTs a webhook to your Svix Ingest Source URL.
  2. Svix verifies the signature, applies any transformations or filters you've configured, and stores the event durably.
  3. Your channel server, running inside Claude Code, polls a Svix Ingest polling endpoint for new events.
  4. For each event, the channel server emits a notifications/claude/channel event into the Claude Code session.
  5. Claude reads it and acts: reading files, running commands, fixing code, whatever the situation calls for.

The nice thing about polling is that the channel server never needs a public URL or open port. It runs as a quiet subprocess of Claude Code, makes outbound HTTPS calls to Svix, and pushes whatever it gets to Claude. No tunnels, no firewall holes.

Step 1: Set up Svix Ingest

A Source in Svix Ingest is the public URL that external providers send webhooks to. It's the one thing in this setup that does need to be reachable from the internet, and Svix hosts it for you.

Sign in to the Svix Ingest dashboard and create a new Source. Pick the provider you're integrating with. For this walkthrough, that's GitHub. Picking the right provider type is what gives you built-in signature verification for Stripe, GitHub, Shopify, Slack, Resend, Zoom, HubSpot, Beehiiv, Replicate, incident.io, and the rest of the supported providers, with no signing-secret code on your side.

Svix gives you back a public ingest URL that looks roughly like:

https://api.svix.com/ingest/api/v1/source/in_2A7Bf...../

Copy this. You'll hand it to GitHub in a later step. The URL is stable: it doesn't change between sessions, so you only have to configure your provider once.

Next, on the same Source, add an endpoint and pick Polling Endpoint as the type. Once it's created, Svix gives you:

  • A unique poller URL like https://api.us.svix.com/api/v1/app/app_2mG.../poller/poll_59q
  • An endpoint-scoped API key like sk_poll_*****.eu that you create from the polling endpoint's settings page

Save both. They go into your channel server's environment in the next step.

Step 2: Build the polling channel server

Create a new directory for your channel and set up the project:

mkdir webhook-channel && cd webhook-channel
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk

Now create a single file called webhook.js. This is the entire channel server, an MCP server with a polling loop:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { setTimeout as sleep } from "node:timers/promises";

const POLLER_URL = process.env.SVIX_POLLER_URL;
const POLLER_KEY = process.env.SVIX_POLLER_KEY;

if (!POLLER_URL || !POLLER_KEY) {
console.error("Set SVIX_POLLER_URL and SVIX_POLLER_KEY");
process.exit(1);
}

const mcp = new McpServer(
{ name: "webhook", version: "1.0.0" },
{ capabilities: { experimental: { "claude/channel": {} } } }
);

// Connect to Claude Code over stdio first, so notifications land somewhere
const transport = new StdioServerTransport();
await mcp.connect(transport);

// Poll Svix Ingest for verified events and push each one into Claude Code
let iterator;

while (true) {
const url = new URL(POLLER_URL);
if (iterator) url.searchParams.set("iterator", iterator);

const res = await fetch(url, {
headers: {
Authorization: `Bearer ${POLLER_KEY}`,
Accept: "application/json",
},
});

if (!res.ok) {
console.error(`[poll] HTTP ${res.status}, retrying in 5s`);
await sleep(5000);
continue;
}

const { data, iterator: next } = await res.json();

for (const event of data) {
await mcp.notification({
method: "notifications/claude/channel",
params: {
content: `${event.type}: ${JSON.stringify(event)}`,
meta: {
source: "svix-ingest",
event_type: event.type,
message_id: event.id,
},
},
});
console.error(`[poll] forwarded ${event.type} to Claude`);
}

iterator = next;

// Back off when there's nothing new. Svix returns immediately on empty pages.
if (data.length === 0) await sleep(2000);
}

Two things worth flagging. All logging uses console.error rather than console.log, because stdout is reserved for MCP communication between the server and Claude Code. The notification uses content for the payload Claude reads and meta for structured fields Claude can reference when deciding how to act. The top-level await works because the project is set up as an ES module ("type": "module" in package.json).

If you'd rather not write the polling loop by hand, the official Svix SDK includes a polling helper that handles pagination and backoff for you. The polling endpoints docs have the details. The plain-fetch version above is just the most explicit thing to read in a tutorial.

Step 3: Register the server with Claude Code

Add the channel server to your .mcp.json. You can put this in the project directory (so it applies to that project) or in ~/.mcp.json (so it's available everywhere). Drop the poller URL and key in as environment variables:

{
"mcpServers": {
"webhook": {
"command": "node",
"args": ["./webhook-channel/webhook.js"],
"env": {
"SVIX_POLLER_URL": "https://api.us.svix.com/api/v1/app/app_2mG.../poller/poll_59q",
"SVIX_POLLER_KEY": "sk_poll_*****.eu"
}
}
}
}

Adjust the path in args to wherever you saved webhook.js. A relative path works for project-level configs; use the full absolute path for ~/.mcp.json.

Next, create a .claude/channels.json in the same location to register the channel:

{
"webhook": {
"server": "webhook"
}
}

This tells Claude Code that the webhook MCP server should be treated as a channel.

Step 4: Configure GitHub to send webhooks

Go to your GitHub repo → SettingsWebhooksAdd webhook.

  • Payload URL: paste the Svix Ingest Source URL from Step 1.
  • Content type: select application/json.
  • Secret: set the same signing secret you configured on the Svix Ingest Source. Svix verifies the GitHub signature on every request before it ever reaches your channel server.
  • Events: pick "Let me select individual events" and check Push events (or whatever you want Claude to react to).
  • Active: make sure it's checked.

Click Add webhook. GitHub immediately sends a ping event, which you should see appear in the Svix Ingest dashboard (under the Source's event log) within a second or two. That confirms ingestion is working end to end before Claude is even in the picture.

Step 5: Start Claude Code with the channel enabled

Channels are in research preview, so custom channels aren't on the approved allowlist yet. Start Claude Code with the development flag:

claude --dangerously-load-development-channels server:webhook

The server:webhook argument refers to the server name in your .mcp.json. When Claude Code starts, it reads the file, spawns webhook.js as a subprocess with the env vars you configured, and the polling loop begins.

If you see blocked by org policy, your Team or Enterprise admin needs to enable channels via the channelsEnabled managed setting.

Step 6: Trigger a webhook and watch Claude react

Push a commit to your repo. Anything works: edit the README, add a line to a file, push a one-character change.

What happens:

  1. GitHub POSTs a push webhook to your Svix Ingest Source.
  2. Svix verifies the GitHub signature, runs any transformations you've set up, and stores the event for replay.
  3. Your channel server's next poll picks up the event (typically within a couple of seconds).
  4. The channel server emits a notifications/claude/channel event with the payload and metadata.
  5. Claude Code reads the push payload (commit message, changed files, author, branch) and starts responding.

Depending on your CLAUDE.md instructions, Claude might review the diff, run tests, file a quick summary, or just acknowledge the event.

Why Svix Ingest and not just ngrok?

You could point GitHub at an ngrok tunnel, write your own tiny HTTP server, and skip Svix entirely. For a one-off it works. But for a channel you actually want to keep around, Svix Ingest gives you a few things that matter:

No public URL on your side. Polling endpoints are an outbound HTTPS call from your laptop to Svix. No tunnel, no exposed port, no firewall changes, and nothing for ngrok to hand you a new URL for every restart. The channel server already runs as a subprocess of Claude Code, so there's nothing else to deploy.

Built-in signature verification. Svix knows how to verify signatures from every major webhook provider (Stripe, GitHub, Shopify, Slack, Resend, Zoom, HubSpot, and the rest) out of the box. Your channel server only ever sees verified events. No signing-secret plumbing in your webhook.js.

Stable URLs. Your Svix Ingest Source URL stays the same across sessions, restarts, and machines. With ngrok on the free tier, you get a new URL every restart and have to reconfigure GitHub each time.

Replay and durability. Svix stores every event it receives and lets you replay it at the click of a button. When you're tweaking your channel server's notification formatting, you can restart Claude Code and replay the same GitHub push event without having to push another commit. The feedback loop drops from minutes to seconds. If your channel server is down when an event arrives, Svix just holds onto it until the next poll. Nothing gets lost.

Transformations and filtering. If you're subscribed to a lot of GitHub events but only want push events to reach Claude, you can filter or reshape payloads in Svix with a few lines of JavaScript before they ever hit your channel server. Less noise for Claude to parse, and a cleaner notification payload.

Observability. The Svix dashboard gives you searchable history of every event, the headers and body that arrived, the signature verification result, and which polls picked it up. When Claude isn't reacting the way you expect, inspecting the actual payload that came in beats adding more console.error lines.

Customizing what Claude does with the events

Out of the box, Claude reads the webhook payload and decides what to do based on context. Guide that behavior by adding instructions to your project's CLAUDE.md:

## Webhook handling

When you receive a GitHub push webhook via the webhook channel:

1. Check which files were changed in the commit
2. If any test files were modified, run the test suite
3. If the commit message mentions "fix" or "bug", review the changed files for potential issues
4. Summarize what was pushed in a short message

You can differentiate by event type using the meta object in the notification: the event_type field tells Claude whether it's a push, pull request, issue, or something else.

Extending the channel server

The webhook.js above is deliberately bare. A few ways to extend it:

Pre-format notifications. Claude handles raw JSON fine, but a one-line human-readable summary in the notification (e.g. alex pushed 3 commits to main: ...) makes it easier to see what's happening at a glance. Even better, do this in a Svix transformation so the channel server stays generic.

Filter at the source. Add a Svix transformation or filter to drop events you don't want Claude to see at all (e.g. bot pushes, draft PRs). This is faster and cheaper than filtering in your channel server, and it keeps Claude's context focused.

Make it two-way. The MCP channel reference shows how to expose a reply tool so Claude can send messages back. Combined with Svix Ingest's fanout, you can have Claude post a comment back on the GitHub PR, acknowledge an alert in your monitoring tool, or trigger a downstream service.

What about production?

Channels are in research preview, so this is development-focused for now. A few things to keep in mind:

  • Events are processed while the Claude Code session is open. For always-on setups, run Claude Code in a persistent terminal or background process. Svix Ingest holds onto events durably, so when the session comes back up the channel server's next poll catches up. Nothing gets lost while Claude is offline.
  • The --dangerously-load-development-channels flag is for testing. Custom channels will eventually go through an approval process or plugin marketplace.
  • If Claude hits a permission prompt while you're away from the terminal, the session pauses until you approve. For unattended use, --dangerously-skip-permissions bypasses this. Only use it in environments you trust.

For a production-grade setup, lean on Svix Ingest's full event gateway: durable storage with retries, payload transformations, FIFO ordering, polling endpoints, and a full audit trail of every event going into your Claude Code session.

Wrapping up

Claude Code Channels turn Claude from a tool you talk to into a process that reacts to your systems. Pairing them with Svix Ingest and a polling endpoint gives you the production-grade plumbing (signature verification, retries, replay, transformations, observability) without any of the pieces that usually make local webhook development painful: no tunnel, no public URL, no signing-secret code.

The free tier is enough to get up and running, and the same Svix Ingest setup scales straight into production when you want to graduate beyond a single laptop.