The Problem: Webhooks Need a Public URL

We have all been there – you are building a Stripe integration, your endpoint is ready, and you realize Stripe cannot reach localhost:8080. A local tunnel fixes this in 30 seconds.

A webhook is an HTTP request that an external service sends to your server when an event occurs. Stripe notifies you about a payment, GitHub about a push, Telegram about a new message to your bot. The catch is that all of these services require a public URL. Your localhost:8080 is not reachable from the internet, and pointing a webhook at http://127.0.0.1 simply does not work.

The classic workarounds — deploying to a staging server after every change, or using request-capture services — are slow and clumsy. You spend minutes deploying just to verify a single line of code. And capture services show you the request body but do not let you step through your handling logic in a debugger.

The Solution: A Local Tunnel for Webhook Testing

A local tunnel creates a public URL that points straight at your localhost. A webhook from Stripe hits https://abc.fxtun.dev, the tunnel server forwards the request to your localhost:8080, and your application receives the data in real time. The development cycle shrinks to seconds.

What this means for you as a developer:

  • Instant feedback — you see webhooks the moment they are sent, with no deploy delay.
  • Inspector + Replay — the built-in web interface shows all incoming requests with headers and body. Replay lets you resend any webhook with one click — no need to re-trigger the event in Stripe or GitHub. Available from $5/mo.
  • Real data — test with actual events from Stripe, GitHub, or Telegram instead of hand-rolled mocks.
  • IDE debugging — set breakpoints and inspect payloads right in your debugger. This is impossible when testing on a remote server.
  • No deployment — everything runs on your local machine. Change a line of code and see the result immediately.
  • Security — test data never leaves your machine. The tunnel encrypts traffic via TLS.

Step-by-Step Guide: Testing Webhooks with fxTunnel

Setup takes 30 seconds: install the CLI, start a tunnel with one command, and paste the public URL into your webhook settings. No need to spin up a server, configure DNS, or manage certificates.

Step 1. Install fxTunnel

# Quick install (Linux/macOS)
curl -fsSL https://fxtun.dev/install.sh | bash

# Verify the installation
fxtunnel --version

Step 2. Start Your Local Server

Make sure your application is running and listening on the correct port. For this example, we will use a simple Express server:

// webhook-server.js
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook', (req, res) => {
  console.log('Webhook received:', JSON.stringify(req.body, null, 2));
  console.log('Headers:', JSON.stringify(req.headers, null, 2));
  res.status(200).json({ received: true });
});

app.listen(8080, () => {
  console.log('Server listening on port 8080');
});
node webhook-server.js
# -> Server listening on port 8080

Step 3. Create a Tunnel

# Open a tunnel to port 8080
fxtunnel http 8080

You will see the public URL:

fxTunnel v1.x — tunnel is active
Public URL:  https://wh-demo.fxtun.dev
Forwarding:  https://wh-demo.fxtun.dev -> http://localhost:8080

Now https://wh-demo.fxtun.dev/webhook is your webhook endpoint.

Step 4. Configure the Webhook in the Service

Enter the full URL https://wh-demo.fxtun.dev/webhook in the settings panel of the service you are integrating with. Details for specific services are below.

Here are ready-to-use examples for Stripe, GitHub, and Telegram. Each one covers endpoint configuration, signature verification, and event handling. All examples use the same tunnel public URL – just run fxtunnel http 8080.

Stripe: Testing Payment Webhooks

Stripe relies heavily on webhooks to notify you about payments, subscriptions, and disputes. Without webhooks, you have no way to learn the outcome of asynchronous operations — for example, a successful 3D Secure payment or a transaction dispute. When building a payment integration, webhook testing is a mandatory step.

Setup via Dashboard

  1. Open the Stripe Dashboard in test mode.
  2. Click Add endpoint.
  3. Enter the URL: https://wh-demo.fxtun.dev/webhook.
  4. Select events: payment_intent.succeeded, checkout.session.completed.
  5. Save and copy the Webhook Signing Secret (whsec_...).

Signature Verification on the Server

// Verify Stripe webhook signature
const stripe = require('stripe')('sk_test_...');
const endpointSecret = 'whsec_...';

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];

  try {
    const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    console.log('Stripe event:', event.type);

    // Handle specific events
    switch (event.type) {
      case 'payment_intent.succeeded':
        console.log('Payment succeeded:', event.data.object.id);
        break;
      case 'checkout.session.completed':
        console.log('Checkout completed:', event.data.object.id);
        break;
    }

    res.status(200).json({ received: true });
  } catch (err) {
    console.error('Signature verification failed:', err.message);
    res.status(400).send(`Webhook Error: ${err.message}`);
  }
});

Sending a Test Event

# Via Stripe CLI (an alternative way to test)
stripe trigger payment_intent.succeeded \
  --override payment_intent:metadata.order_id=test_123

GitHub: Webhook for Push Events

Setup

  1. Open your repository on GitHub, then go to Settings and click on Webhooks.
  2. Click Add webhook.
  3. Payload URL: https://wh-demo.fxtun.dev/webhook.
  4. Content type: application/json.
  5. Secret: create a secret for signature verification.
  6. Select the event: Just the push event.

Server-Side Handler

const crypto = require('crypto');

const GITHUB_SECRET = 'your_webhook_secret';

app.post('/webhook', (req, res) => {
  // Verify GitHub signature
  const signature = req.headers['x-hub-signature-256'];
  const hmac = crypto.createHmac('sha256', GITHUB_SECRET);
  const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');

  if (signature !== digest) {
    return res.status(401).send('Invalid signature');
  }

  const event = req.headers['x-github-event'];
  console.log(`GitHub event: ${event}`);

  if (event === 'push') {
    const { ref, commits, pusher } = req.body;
    console.log(`Push to ${ref} by ${pusher.name}`);
    console.log(`Commits: ${commits.length}`);
    commits.forEach(c => console.log(`  - ${c.message}`));
  }

  res.status(200).json({ ok: true });
});

Telegram Bot: setWebhook

Telegram bots can operate in two modes: polling (the bot periodically asks the server for new messages) or webhooks (Telegram pushes messages to your URL). Webhooks are faster, more efficient, and use fewer resources. To develop a bot with webhooks, you need a public HTTPS address — exactly what a tunnel provides.

Registering the Webhook

# Set the webhook via the Telegram Bot API
curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://wh-demo.fxtun.dev/webhook"}'

# Check the status
curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getWebhookInfo"

Handling Messages

app.post('/webhook', (req, res) => {
  const { message } = req.body;

  if (message && message.text) {
    console.log(`Message from ${message.from.first_name}: ${message.text}`);

    // Reply to the user
    const chatId = message.chat.id;
    const replyText = `You said: ${message.text}`;

    fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ chat_id: chatId, text: replyText }),
    });
  }

  res.status(200).json({ ok: true });
});

Remove the Webhook After Testing

# Delete the webhook when you are done testing
curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/deleteWebhook"

Troubleshooting: Webhook Testing Issues with a Tunnel

Most webhook problems come down to three things: the tunnel is not running, the local server is not listening, or the URL is wrong. The Inspector in fxTunnel (from $5/mo) shows whether the request reached the tunnel, which immediately narrows down the issue.

ProblemPossible causeSolution
Webhook not arrivingTunnel is not runningMake sure fxtunnel is running and the URL is reachable in a browser
HTTP 502 Bad GatewayLocal server is not runningStart your application on the correct port before creating the tunnel
HTTP 401 UnauthorizedInvalid webhook signatureVerify that the secret matches in both the service settings and your code
Request timeoutHandler takes too longRespond with 200 OK immediately and process the event asynchronously
Empty payloadWrong Content-TypeMake sure express.json() middleware is loaded and the route path matches
URL changed after restartRandom subdomainUse a custom domain in fxTunnel (from $5/mo)
Cannot see incoming dataNo inspection toolOpen the fxTunnel Inspector — it shows all requests with headers and body
Need to resend a requestEvent is hard to reproduceUse Replay in fxTunnel — resend any request with one click

Webhook Testing Best Practices for Localhost Development

A solid approach to webhook testing saves hours of debugging. Here are five principles worth following from day one.

1. Always Verify the Signature

Do not skip signature verification even during development. Catching configuration errors early prevents surprises at deploy time.

2. Log Everything (or Use Inspector)

Instead of manual logging, you can use the Inspector in fxTunnel — it shows all incoming requests in a web interface with headers, body, and response status. But explicit server-side logging is still useful:

// Log the full request for debugging
app.post('/webhook', (req, res) => {
  console.log('--- Incoming webhook ---');
  console.log('Method:', req.method);
  console.log('Path:', req.path);
  console.log('Headers:', JSON.stringify(req.headers, null, 2));
  console.log('Body:', JSON.stringify(req.body, null, 2));
  console.log('--- End ---');
  res.status(200).json({ received: true });
});

3. Respond Quickly

Most services expect a response within 5 to 30 seconds. If your processing takes longer, respond with 200 OK right away and queue the work for background processing.

4. Handle Retries

Services will resend webhooks if they do not receive a 2xx response. Implement idempotency: check the unique event ID before processing to avoid handling the same event twice.

5. Use Test Mode

Stripe, GitHub, and other services offer a test mode or the ability to send test events. Use these before configuring real data flows.

Why fxTunnel Is the Best Tool for Webhook Testing

Here is how fxTunnel stacks up against other tools for webhook testing specifically. We go deeper in the full comparison.

FeaturefxTunnelOther tools
Free tierNo limits on traffic or connectionsOften limited by time, requests, or connections
Inspector + ReplayFrom $5/mo — view all requests and resend with one clickMissing or requires a separate tool
SetupOne command, 30 secondsOften requires configuration, tokens, registration
HTTP / TCP / UDPAll three protocolsUsually HTTP only
Custom domainFrom $5/mo, up to 5 tunnelsMore expensive or unavailable
10+ tunnelsFrom $10/moSignificantly more expensive
Open sourceYes, self-hosted option availableOften proprietary

We also cover these tools in Best Tunneling Tools 2026.

FAQ

Why do webhooks not reach localhost?

The address 127.0.0.1 (localhost) only exists inside your machine – it is not routable over the public internet. Services like Stripe, GitHub, and Telegram have no way to send an HTTP request to it. You need a public URL that forwards to your local port, which is exactly what a local tunnel provides.

Is it safe to receive webhooks through a tunnel?

For development and testing, absolutely. Traffic is encrypted via TLS, and you should always verify webhook signatures on your application side regardless of how the request arrives. Just keep in mind that tunnels are meant for dev workflows, not production webhook endpoints.

Do I need to change the webhook URL every time I restart the tunnel?

With fxTunnel SaaS, the URL persists across restarts – even on the free tier, so you do not need to update webhook settings every time. If you want a fully branded URL, custom domains are available from $5/mo.

How do I debug a webhook when no data is coming through?

Start with the basics: is the tunnel running? Can you open the URL in a browser? Is the URL entered correctly in the service settings? Is your local server actually listening on the right port? The built-in Inspector in fxTunnel (from $5/mo) also helps by showing all incoming requests in real time with headers and body.

What is Inspector and Replay in fxTunnel?

Inspector is a web UI that shows every HTTP request passing through the tunnel – headers, body, response status, all of it. Replay lets you resend any captured request with one click, which is invaluable when you need to re-trigger a webhook without reproducing the original event. Both are available from $5/mo.