How to Test Localhost APIs with Webhook Services ?

Learn how to test localhost APIs with webhook tunneling services like ngrok, NPort, and Cloudflare Tunnel. Complete guide with Apidog integration for testing webhooks, OAuth callbacks, and third-party integrations

Ashley Innocent

Ashley Innocent

28 January 2026

How to Test Localhost APIs with Webhook Services ?

Testing localhost APIs that need to receive webhooks or callbacks from external services requires exposing your local development server to the internet temporarily. Tunneling services like ngrok, NPort, Cloudflare Tunnel, and others create secure connections that give your localhost a public URL.

💡
Download Apidog to follow along with the webhook testing workflows in this guide. This guide covers how to choose the right tool, set up tunneling, test webhooks effectively using Apidog, and handle common challenges like authentication, rate limits, and debugging.
button

Why You Need Localhost Tunneling

You're building an API that integrates with third-party services. Everything works on your laptop—endpoints respond correctly, data flows smoothly. Then you try to test webhook callbacks from Stripe, GitHub, Twilio, or any external service.

Problem: External services can't reach localhost:3000. Your development server isn't accessible from the internet.

Common scenarios where this breaks your workflow:

1. Webhook Testing

Services like Stripe send payment confirmations, GitHub sends repository events, Slack sends interaction events—all as POST requests to your API. During development, these services need a public URL to send webhooks to.

2. OAuth Callback URLs

When implementing "Sign in with Google," "Login with GitHub," or any OAuth flow, the authentication provider redirects users back to your application with an authorization code. The redirect URL must be publicly accessible and match what you registered with the provider.

3. Third-Party API Integration

Some APIs require callback URLs for asynchronous operations. For example, video transcoding services notify your API when processing completes, or payment processors confirm transactions.

4. Mobile App Development

Testing your API from a mobile device on the same network often fails because the mobile app can't resolve localhost. A tunnel gives you a URL that works from any device.

5. Client Demos

Sometimes you need to show in-progress work to clients or stakeholders. Deploying to staging for every small change slows iteration. A temporary public URL lets clients test your development environment.

How Localhost Tunneling Works

Tunneling services create a secure connection between their cloud servers and your local machine:

External Service → Tunneling Service (public URL) → Secure Connection → Your Localhost:3000

The process:

  1. You start a tunnel client on your machine pointing to your local port
  2. The client connects to the tunneling service's cloud infrastructure
  3. The service assigns a public URL (e.g., https://abc123.ngrok.io)
  4. Incoming requests to that public URL are forwarded through the encrypted connection to your localhost
  5. Your local server receives the request as if it came directly from the client
  6. Responses flow back through the tunnel to the requester

This happens transparently. Your local server doesn't need to know it's behind a tunnel.

Here are the most popular options in 2026, with their strengths and limitations:

Best for: Established projects, teams wanting reliability

ngrok http 3000

Pros:

Cons:

Free Tier:

Paid Plans: $8-$20/month

NPort (Rising Free Alternative)

Best for: Developers avoiding subscription costs

nport start 3000

Pros:

Cons:

Free Tier:

GitHub - tuanngocptn/nport: NPort is a powerful, lightweight ngrok alternative that creates secure HTTP/HTTPS tunnels from your localhost to public URLs using Cloudflare’s global edge network. No configuration, no accounts, just instant tunnels with custom subdomains!
NPort is a powerful, lightweight ngrok alternative that creates secure HTTP/HTTPS tunnels from your localhost to public URLs using Cloudflare's global edge network. No configuration, no account...

This is the tool gaining traction on Dev.to as developers seek ngrok alternatives without ongoing costs.

Cloudflare Tunnel (Best for Production-Adjacent)

Best for: Teams already using Cloudflare, long-running tunnels

cloudflared tunnel --url http://localhost:3000

Pros:

Cons:

Free Tier:

Localtunnel (Simplest)

Best for: Quick one-off tests, no installation

npx localtunnel --port 3000

Pros:

Cons:

Cloudflare Tunnel
Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address. With Tunnel, you do not send traffic to an external IP — instead, a lightweight daemon in your infrastructure (cloudflared) creates outbound-only connections to Cloudflare…

Free Tier:

Tailscale Funnel (Best for Teams)

Best for: Private team sharing, secure demos

tailscale serve https / http://localhost:3000
tailscale funnel 443 on

Pros:

Cons:

Free Tier:

Tailscale Funnel · Tailscale Docs
Securely route internet traffic to local services using Tailscale Funnel.

Comparison Table

FeaturengrokNPortCloudflare TunnelLocaltunnelTailscale
PriceFree/$10+FreeFreeFreeFree/Paid
Session Limit2 hoursNoneNoneNoneNone
Custom DomainPaidFreeYesNoYes
Request InspectorYesBasicNoNoNo
Setup ComplexityLowLowMediumVery LowMedium
ReliabilityExcellentGoodExcellentPoorExcellent
Best ForProduction testingCost-conscious devsEnterpriseQuick testsTeam sharing

Setting Up Your First Localhost Tunnel

Let's walk through setup with the most common tools. We'll use a Node.js Express API as an example, but this works with any local server.

Example: Local API Server

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

app.use(express.json());

app.post('/webhook', (req, res) => {
  console.log('Webhook received:', req.body);
  res.json({ received: true });
});

app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Option 1: Using ngrok

Step 1: Install ngrok

# macOS
brew install ngrok

# Windows (via Chocolatey)
choco install ngrok

# Linux
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \
  sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null && \
  echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \
  sudo tee /etc/apt/sources.list.d/ngrok.list && \
  sudo apt update && sudo apt install ngrok

Step 2: Authenticate (optional but recommended)

ngrok config add-authtoken YOUR_AUTH_TOKEN

Step 3: Start the tunnel

ngrok http 3000

Output:

Session Status                online
Account                       you@example.com (Plan: Free)
Version                       3.5.0
Region                        United States (us)
Forwarding                    https://abc123.ngrok.io -> http://localhost:3000

Your API is now accessible at https://abc123.ngrok.io.

Step 4: Test it

curl https://abc123.ngrok.io/health
# {"status":"healthy"}

Option 2: Using NPort (Free Alternative)

Step 1: Install NPort

npm install -g nport-cli
# or
curl -sSL https://nport.io/install.sh | bash

Step 2: Start the tunnel

nport start 3000 --subdomain myapi

Output:

✓ Tunnel started successfully
Public URL: https://myapi.nport.io
Local URL:  http://localhost:3000

Step 3: Test it

curl https://myapi.nport.io/health
# {"status":"healthy"}

Option 3: Using Cloudflare Tunnel

Step 1: Install cloudflared

# macOS
brew install cloudflare/cloudflare/cloudflared

# Linux
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb

Step 2: Quick tunnel (no signup needed)

cloudflared tunnel --url http://localhost:3000

Output:

2026-01-27T12:00:00Z INF Your quick tunnel is: https://xyz789.trycloudflare.com

For persistent tunnels (requires Cloudflare account):

# Login
cloudflared tunnel login

# Create tunnel
cloudflared tunnel create myapi

# Configure and run
cloudflared tunnel --config config.yml run myapi

Testing Webhooks with Apidog

Now that your localhost is publicly accessible, let's test webhooks systematically using Apidog.

Why Combine Tunneling + Apidog?

Tunneling solves access; Apidog solves verification:

Setting Up Webhook Testing in Apidog

Step 1: Import or Create Your API

  1. Open Apidog

2. Create a new project

3. Add your webhook endpoint:

Step 2: Configure Environment Variables

Set up two environments:

Development (Tunneled):

{
  "base_url": "https://abc123.ngrok.io"
}

Production:

{
  "base_url": "https://api.yourapp.com"
}

This lets you test the same endpoint locally and in production with one click.

Step 3: Create Test Scenarios

Test what happens when webhooks arrive:

Example: Stripe Payment Webhook Test

// Request Body
{
  "type": "payment_intent.succeeded",
  "data": {
    "object": {
      "id": "pi_test123",
      "amount": 2000,
      "currency": "usd",
      "status": "succeeded"
    }
  }
}

Assertions in Apidog:

  1. Status code equals 200
  2. Response contains received: true
  3. Response time < 1000ms
  4. Content-Type is application/json

Step 4: Simulate Third-Party Services

Instead of triggering real webhooks from Stripe or GitHub, simulate them in Apidog:

  1. Copy webhook payload examples from service documentation
  2. Create test cases with various scenarios (success, failure, edge cases)
  3. Run all scenarios against your tunneled localhost
  4. Verify your API handles each case correctly

Testing OAuth Callbacks

Scenario: You're implementing "Sign in with Google"

Step 1: Start tunnel with custom subdomain

ngrok http 3000 --subdomain myapp
# URL: https://myapp.ngrok.io

Step 2: Configure OAuth redirect in Google Console

Set callback URL: https://myapp.ngrok.io/auth/google/callback

Step 3: Test the flow in Apidog

  1. Make request to /auth/google to get authorization URL
  2. Follow redirect manually or programmatically
  3. Verify callback receives authorization code
  4. Assert token exchange works correctly

Step 4: Validate token storage

Use Apidog to:

Common Use Cases

1. Testing Payment Webhooks (Stripe, PayPal)

Challenge: Payment providers send webhooks for events like successful charges, refunds, disputes.

Solution:

# Start tunnel
ngrok http 3000

# Configure webhook URL in Stripe dashboard
# https://abc123.ngrok.io/webhook/stripe

# Use Stripe CLI to forward test webhooks
stripe listen --forward-to localhost:3000/webhook/stripe

# Trigger test events
stripe trigger payment_intent.succeeded

Test with Apidog:

2. Testing Slack/Discord Bot Commands

Challenge: Chat platforms send interaction events when users click buttons or run commands.

Solution:

# Start tunnel
nport start 3000 --subdomain myslackbot

# Configure in Slack API:
# Interactivity URL: https://myslackbot.nport.io/slack/interactions
# Slash Commands: https://myslackbot.nport.io/slack/commands

Test with Apidog:

3. Testing SMS/Voice Webhooks (Twilio)

Challenge: Twilio sends webhooks when SMS arrives or voice calls are received.

Solution:

cloudflared tunnel --url http://localhost:3000

Configure TwiML webhooks to point to your tunnel URL.

Test with Apidog:

4. Mobile App API Testing

Challenge: Testing your API from a physical device or emulator.

Problem with localhost:

// This fails from mobile device
fetch('http://localhost:3000/api/users')

Solution with tunnel:

// This works from anywhere
fetch('https://myapi.ngrok.io/api/users')

Test with Apidog:

  1. Generate API documentation with tunneled base URL
  2. Share with mobile team
  3. Mobile devs can test against your live development server
  4. Switch to staging/production URLs when ready

5. Testing GitHub/GitLab Webhooks

Challenge: Testing repository webhooks (push, pull request, issues) locally.

Solution:

# Start tunnel
ngrok http 4000

# Configure in GitHub repo settings:
# Webhook URL: https://abc123.ngrok.io/github/webhook
# Content type: application/json
# Events: Push, Pull requests

Test with Apidog:

Security Best Practices

Exposing localhost to the internet creates security risks. Follow these practices:

1. Use HTTPS Only

All tunneling services provide HTTPS by default. Never use plain HTTP for tunnels:

# Good
ngrok http 3000
# Creates https://abc123.ngrok.io

# Bad (don't do this)
ngrok http --scheme=http 3000

2. Implement Webhook Signature Verification

Don't trust incoming webhooks blindly. Verify signatures:

const crypto = require('crypto');

function verifyStripeSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhook/stripe', (req, res) => {
  const signature = req.headers['stripe-signature'];

  if (!verifyStripeSignature(req.body, signature, process.env.STRIPE_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook
});

3. Restrict Access with Basic Auth

Add authentication to your tunnel:

# ngrok with basic auth
ngrok http 3000 --auth="username:password"

# NPort with basic auth
nport start 3000 --auth username:password

Now requests need credentials:

curl -u username:password https://abc123.ngrok.io/webhook

4. Use Environment-Specific Secrets

Never commit webhook secrets or API keys:

// .env.development
STRIPE_WEBHOOK_SECRET=whsec_test_abc123
WEBHOOK_TUNNEL_URL=https://abc123.ngrok.io

// .env.production
STRIPE_WEBHOOK_SECRET=whsec_live_xyz789
WEBHOOK_URL=https://api.yourapp.com

5. Monitor Tunnel Access

Use the request inspector to watch for suspicious activity:

# ngrok provides a web interface at:
http://localhost:4040

# View all requests, responses, replay attacks

6. Limit Tunnel Duration

Don't leave tunnels running indefinitely:

# Auto-expire tunnel after 1 hour
ngrok http 3000 --session-duration 1h

7. Validate Request Sources

Check incoming IP addresses or use allowlists:

const allowedIPs = [
  '192.0.2.1',  // Stripe webhook IPs
  '198.51.100.0/24'
];

app.use('/webhook', (req, res, next) => {
  const clientIP = req.ip;

  if (!allowedIPs.includes(clientIP)) {
    return res.status(403).send('Forbidden');
  }

  next();
});

Troubleshooting Common Issues

Issue 1: Tunnel URL Changes Every Session

Problem: Free ngrok tunnels use random URLs that change each restart. Webhooks configured with the old URL break.

Solutions:

  1. Use paid plan for static URLs:
ngrok http 3000 --domain=myapp.ngrok.app
  1. Switch to NPort with free custom subdomains:
nport start 3000 --subdomain myapp
# Always https://myapp.nport.io
  1. Update webhooks programmatically via API when tunnel starts

Issue 2: Webhooks Timeout

Problem: Your local server takes too long to respond. Services like Slack require responses within 3 seconds.

Solution:

Process asynchronously:

app.post('/webhook', async (req, res) => {
  // Acknowledge immediately
  res.json({ received: true });

  // Process in background
  processWebhookAsync(req.body).catch(console.error);
});

async function processWebhookAsync(data) {
  // Do slow work here (database, external APIs, etc.)
  await heavyProcessing(data);
}

Test timeouts with Apidog by setting aggressive timeout limits in test scenarios.

Issue 3: CORS Errors from Browser

Problem: Frontend making requests to tunnel URL gets CORS errors.

Solution:

Configure CORS headers:

const cors = require('cors');

app.use(cors({
  origin: [
    'http://localhost:3001',  // Your frontend dev server
    'https://abc123.ngrok.io'  // Your tunnel URL
  ],
  credentials: true
}));

Issue 4: Rate Limiting on Free Tier

Problem: Free tunnels have connection limits (ngrok: 40/min).

Solutions:

  1. Batch test requests in Apidog instead of rapid-fire individual tests
  2. Use multiple tunnels for different services
  3. Upgrade to paid tier if testing heavily
  4. Switch to unlimited service like Cloudflare Tunnel or NPort

Issue 5: Tunnel Disconnects Frequently

Problem: Network instability causes tunnel drops.

Solution:

Use systemd/pm2 to auto-restart:

# Create systemd service
sudo nano /etc/systemd/system/ngrok.service
[Unit]
Description=ngrok tunnel
After=network.target

[Service]
Type=simple
User=youruser
WorkingDirectory=/home/youruser
ExecStart=/usr/local/bin/ngrok http 3000
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl enable ngrok
sudo systemctl start ngrok

Issue 6: Cannot Reach Tunnel from Specific Network

Problem: Corporate firewalls or restrictive networks block tunnel traffic.

Solutions:

  1. Use Cloudflare Tunnel (rarely blocked)
  2. Switch tunnel region closer to you:
ngrok http 3000 --region eu
  1. Use Tailscale for private network instead of public tunnel

Advanced Patterns

Pattern 1: Multi-Port Tunneling

Expose multiple services simultaneously:

# Terminal 1: API server
ngrok http 3000

# Terminal 2: Frontend dev server
ngrok http 3001

# Terminal 3: Webhook worker
ngrok http 3002

Or use ngrok config file:

# ~/.ngrok2/ngrok.yml
tunnels:
  api:
    proto: http
    addr: 3000
  frontend:
    proto: http
    addr: 3001
  worker:
    proto: http
    addr: 3002
ngrok start --all

Pattern 2: Tunnel + Docker Compose

# docker-compose.yml
version: '3'
services:
  api:
    build: .
    ports:
      - "3000:3000"

  ngrok:
    image: ngrok/ngrok:latest
    command:
      - "http"
      - "api:3000"
    environment:
      NGROK_AUTHTOKEN: ${NGROK_AUTHTOKEN}
docker-compose up

Pattern 3: Dynamic Tunnel URL Injection

Automatically update your app with tunnel URL:

// start-tunnel.js
const ngrok = require('ngrok');
const fs = require('fs');

(async function() {
  const url = await ngrok.connect(3000);
  console.log(`Tunnel started: ${url}`);

  // Update .env file
  fs.appendFileSync('.env', `\nTUNNEL_URL=${url}\n`);

  // Update Stripe webhook
  await updateStripeWebhook(url);
})();

Pattern 4: Request Forwarding to Multiple Environments

Test the same webhook against dev, staging, and production:

// webhook-multiplexer.js
app.post('/webhook', async (req, res) => {
  const environments = [
    'http://localhost:3000',
    'https://staging.api.com',
    'https://api.yourapp.com'
  ];

  // Forward to all environments
  const results = await Promise.all(
    environments.map(env =>
      fetch(`${env}/webhook`, {
        method: 'POST',
        headers: req.headers,
        body: JSON.stringify(req.body)
      })
    )
  );

  res.json({ forwarded: results.length });
});

Conclusion

Testing localhost APIs that receive webhooks or callbacks doesn't require deploying to staging for every change. Tunneling services create temporary public URLs that let external services reach your development environment.

Start with the free tier of any tool. If webhook testing becomes a daily part of your workflow, consider paid plans for static URLs and additional features. But for most developers, free tunneling services combined with Apidog's API testing capabilities provide everything needed to test localhost APIs effectively.

button

Explore more

How to Self-Host Clawdbot(Moltbot) on Mac Mini or VPS?

How to Self-Host Clawdbot(Moltbot) on Mac Mini or VPS?

Learn how to self-host Clawdbot, the open-source AI assistant, on Mac Mini or VPS for privacy and control. This guide details setup, security, integrations including Apidog for API testing, and troubleshooting to boost productivity.

28 January 2026

How to Use DeepSeek-OCR 2 ?

How to Use DeepSeek-OCR 2 ?

Master DeepSeek-OCR 2 API for document extraction. Learn Visual Causal Flow, vLLM integration, and Python code examples. Process 200K+ pages daily. Try Apidog free.

27 January 2026

How to Use Kimi K2.5 API

How to Use Kimi K2.5 API

Discover how to integrate the powerful Kimi K2.5 API into your applications for advanced multimodal AI tasks. This guide covers setup, authentication, code examples, and best practices using tools like Apidog for seamless testing.

27 January 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs