Skip to content

How to Test Webhooks Locally

Send real webhook payloads from Stripe, GitHub, and Shopify to your localhost development server. Capture, inspect, replay, and debug webhooks without deploying.

Updated Mar 2026

Testing webhooks during development is hard. External services cannot reach localhost, signatures break when you proxy through generic tunneling tools, and real webhook events fire at unpredictable times with unpredictable payloads. webhooks.cc gives you three approaches that solve different parts of the problem: capture and replay real webhooks, send signed payloads directly to localhost, or tunnel live webhooks to your local server in real-time.

What you'll build

By the end of this guide, you will be able to:

  • Capture incoming webhooks from any provider and inspect their full payloads
  • Replay captured requests to your localhost server on demand
  • Send signed test webhooks directly to your local handler with sendTo
  • Set up a real-time tunnel that forwards live webhooks to localhost

You need a webhooks.cc account and an API key from your account page. The free plan includes 50 requests per day, enough for local development.

Prerequisites

  • Node.js 18+ (for SDK) or any HTTP client (for manual testing)
  • A local server running on any port (e.g., localhost:3000)
  • An API key from your account page -- set it as WHK_API_KEY

Method 1: Capture and replay

Capture real webhooks from your provider, then replay them against your local server as many times as you need. This is the best approach when you want to test with actual production-shaped payloads.

1

Create an endpoint

Create an endpoint from the dashboard, CLI, or SDK. Each endpoint gets a unique URL.

# CLI -- creates an endpoint and prints the URL
whk create my-test
// SDK
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
const endpoint = await client.endpoints.create({ name: "my-test" });
console.log(endpoint.url);
// https://go.webhooks.cc/w/abc123
2

Point your provider at the endpoint

Copy the endpoint URL and paste it into your webhook provider's settings. For example, in the Stripe Dashboard, go to Developers > Webhooks > Add endpoint and enter your endpoint URL.

The endpoint accepts any HTTP method, content type, and body. No configuration needed on the webhooks.cc side.

3

Trigger a webhook

Perform the action that triggers a webhook from your provider. For Stripe, that might be completing a test-mode checkout. For GitHub, push a commit to a repository with a webhook configured.

4

Inspect the captured request

Open the dashboard. The captured request appears in real-time -- no refresh needed. You can inspect:

  • HTTP method, path, and query parameters
  • All request headers (including signature headers)
  • Full request body with JSON pretty-printing
  • Source IP address and timestamp

You can also retrieve captured requests programmatically:

const requests = await client.requests.list(endpoint.slug);
const latest = requests[0];
console.log(latest.method, latest.headers, latest.body);
5

Replay to localhost

Replay the captured request to your local server. The replay sends the original method, headers, and body -- your handler processes it as if the provider sent it directly.

const res = await client.requests.replay(latest.id, "http://localhost:3000/api/webhooks");
console.log(res.status); // 200

From the CLI:

whk replay <request-id> --target http://localhost:3000/api/webhooks

Replay as many times as you need. Edit your handler, replay again, iterate until it works.

Method 2: Send signed payloads directly

Send webhooks straight to your localhost handler with proper provider signatures. No webhooks.cc endpoint needed -- the request goes directly from your machine to localhost. This is the best approach for automated tests and deterministic payloads.

1

Install the SDK

npm install @webhooks-cc/sdk
2

Send a signed webhook

Use sendTo to send a webhook directly to your local server. The SDK computes the correct provider signature and includes the appropriate headers.

import { WebhooksCC } from "@webhooks-cc/sdk";
 
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
 
const res = await client.sendTo("http://localhost:3000/api/webhooks/stripe", {
  provider: "stripe",
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
  body: {
    type: "checkout.session.completed",
    data: {
      object: {
        id: "cs_test_123",
        amount_total: 4999,
        currency: "usd",
        payment_status: "paid",
      },
    },
  },
});
 
console.log(res.status); // 200

The stripe-signature header is computed using your webhook secret, so your handler's existing signature verification works without changes.

3

Use provider templates

For common event types, use built-in templates instead of writing the full payload:

const res = await client.sendTo("http://localhost:3000/api/webhooks/stripe", {
  provider: "stripe",
  template: "checkout.session.completed",
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});

Supported providers: stripe, github, shopify, twilio, slack, paddle, linear, standard-webhooks (Polar, Svix, Clerk, Resend).

Method 3: Real-time tunnel

Forward live webhooks to your local server in real-time. The CLI creates an outbound connection from your machine -- no port forwarding, firewall changes, or public IP required.

1

Install the CLI

# macOS / Linux
brew install webhooks-cc/tap/whk
 
# Or install directly with Go
go install github.com/webhooks-cc/cli/cmd/whk@latest
2

Start the tunnel

whk tunnel 3000

This creates an endpoint, prints its URL, and forwards every incoming request to localhost:3000. Configure this URL in your webhook provider.

3

Receive webhooks in real-time

Webhooks arrive at your local server as they happen. The CLI preserves the original method, headers, and body. Path segments after the slug are preserved too:

POST https://go.webhooks.cc/w/abc123/api/webhooks
  -> POST http://localhost:3000/api/webhooks

The terminal shows each forwarded request with its status code and latency.

4

Use an existing endpoint

If you already have an endpoint configured in your provider, forward it without creating a new one:

whk tunnel 3000 --endpoint abc123

Add -e to delete the endpoint when you stop the tunnel:

whk tunnel 3000 -e

How it works

The three methods use different parts of the webhooks.cc infrastructure:

Capture and replay -- The Rust receiver captures incoming requests in under 1ms and writes them directly to Postgres via a single stored procedure. The dashboard displays them in real-time via Supabase Realtime (postgres_changes). Replay re-sends the stored request data to your target URL.

sendTo -- The SDK sends the request directly from your machine to your localhost URL. When you specify a provider, the SDK computes the correct cryptographic signature (HMAC-SHA256 for most providers, Ed25519 for Discord) and includes the provider-specific headers. No intermediary server is involved.

Tunneling -- The CLI opens a Server-Sent Events (SSE) connection to webhooks.cc. When a webhook arrives at your endpoint, the server pushes it through the SSE stream. The CLI reconstructs the request and sends it to your local port. The connection is outbound-only -- your machine initiates it, so no firewall rules or port exposure is needed.

Common issues

"Connection refused" -- Your local server is not running, or the port does not match. Verify your server is listening: curl http://localhost:3000/health. Check that you passed the correct port to whk tunnel or the correct URL to sendTo.

"Signature verification failed" -- The signing secret does not match. For Stripe, use the whsec_... secret from Dashboard > Developers > Webhooks, not your Stripe API key. For GitHub, use the secret you set when creating the webhook, not your personal access token.

"Request never arrives" -- Check the endpoint slug. Verify the external service is sending to the correct URL (https://go.webhooks.cc/w/{slug}). Check the dashboard to see if the request was captured -- if it is there but not forwarded, the tunnel may have disconnected.

"Handler returns 400 or 500" -- Read your handler's error output. Common cause: your framework parses the request body before your webhook handler reads it. Stripe and other providers require the raw body for signature verification. In Next.js, use export const config = { api: { bodyParser: false } } or the App Router raw body approach.

Next steps

CLI Tunneling

Full reference for whk tunnel options and behavior.

Testing with the SDK

CI/CD integration patterns and test utilities.

Mock Responses

Control what your endpoint returns to webhook senders.

Signature Verification

Verify webhook signatures for Stripe, GitHub, Shopify, and 6 more providers.