Stripe sends webhook events — checkout.session.completed, invoice.paid, payment_intent.succeeded — to a URL you control. During development, that URL is localhost. Stripe can't reach it.
Most developers reach for stripe listen --forward-to, but the Stripe CLI only works for Stripe, doesn't support programmatic assertions, and doesn't give you a persistent URL to share with your team.
webhooks.cc gives you a public URL that tunnels to localhost, a dashboard for inspecting every payload, and a TypeScript SDK for writing assertions against captured requests.
What you'll build
By the end of this guide you'll have:
- A public tunnel URL forwarding Stripe events to your local server
- Signature verification on captured requests
- Automated test assertions using the SDK
Install the CLI and authenticate
Install the webhooks.cc CLI:
brew install webhooks-cc/tap/whkThen log in:
whk auth loginThis opens your browser for OAuth. The CLI stores a 90-day API key at ~/.config/whk/token.json.
Start a tunnel
Point the tunnel at the port your app runs on:
whk tunnel 3000Output:
✓ Endpoint created: https://go.webhooks.cc/w/abc123
✓ Forwarding to http://localhost:3000
✓ Dashboard: https://webhooks.cc/dashboard
Every request that hits https://go.webhooks.cc/w/abc123/... gets forwarded to localhost:3000 with the original method, headers, path, and body intact.
Configure Stripe
In the Stripe Dashboard, go to Developers → Webhooks → Add endpoint. Set the endpoint URL to your tunnel URL — for example, https://go.webhooks.cc/w/abc123/stripe. Select the events you need (checkout.session.completed, invoice.paid, etc.).
Stripe sends a test event when you save. You'll see it appear in both the webhooks.cc dashboard and your terminal.
Copy the signing secret from Stripe (starts with whsec_). You'll use it for signature verification.
Write your webhook handler
A minimal Express handler that receives and verifies Stripe events:
import express from "express";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const app = express();
app.post(
"/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["stripe-signature"] as string;
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case "checkout.session.completed":
console.log("Checkout completed:", event.data.object.id);
break;
case "invoice.paid":
console.log("Invoice paid:", event.data.object.id);
break;
}
res.json({ received: true });
}
);
app.listen(3000);Verify signatures on captured requests
The SDK can verify Stripe signatures on any captured request — useful for automated testing outside your handler:
import {
WebhooksCC,
matchHeader,
verifySignature,
isStripeWebhook,
} from "@webhooks-cc/sdk";
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
const request = await client.requests.waitFor("abc123", {
timeout: "30s",
match: matchHeader("stripe-signature"),
});
if (isStripeWebhook(request)) {
const result = await verifySignature(request, {
provider: "stripe",
secret: process.env.STRIPE_WEBHOOK_SECRET!,
});
console.log("Signature valid:", result.valid);
}waitFor polls until a matching request arrives or the timeout expires. The matchHeader matcher filters for requests that carry a stripe-signature header.
Debug with the dashboard
Open https://webhooks.cc/dashboard to see every captured request in a split-pane viewer:
- Method, path, and timestamp
- Full headers (including
stripe-signature) - Parsed JSON body
You can replay any request from the dashboard to re-test your handler without retriggering the event in Stripe. Or replay from code:
await client.requests.replay(request.id, "http://localhost:3000/stripe");Automate the full cycle with the flow builder
The SDK's flow builder chains the complete sequence — create endpoint, send a signed Stripe payload, wait for capture, verify the signature, and clean up — into a single call:
const result = await client
.flow()
.createEndpoint({ expiresIn: "1h" })
.sendTemplate({
provider: "stripe",
template: "checkout.session.completed",
secret: "whsec_test_123",
})
.waitForCapture({ timeout: "15s" })
.verifySignature({
provider: "stripe",
secret: "whsec_test_123",
})
.cleanup()
.run();
console.log(result.verification?.valid); // truesendTemplate generates a properly signed Stripe payload. You don't need to construct the webhook body or compute the t=... signature yourself.
FAQ
CLI Reference
All CLI commands including tunnel, listen, create, and replay.
SDK Reference
Full API reference for endpoints, requests, matchers, and signature verification.