DEV Community

Cover image for Testing Webhooks Locally: Why I Built a Better Alternative to ngrok
abhineet kumar
abhineet kumar

Posted on

Testing Webhooks Locally: Why I Built a Better Alternative to ngrok

Testing Webhooks Locally: Why I Built a Better Alternative to ngrok

If you've ever built webhook integrations, you know the pain: testing webhooks locally is a nightmare. You either expose your local server to the internet (security risk) or use tunneling tools like ngrok that change URLs every time you restart (frustrating).

After building webhook integrations for years, I got tired of these limitations and built Volley CLI - a tool that makes local webhook testing actually enjoyable. Here's why it's better and how to use it.

The Problem with Traditional Approaches

ngrok and Similar Tools

Most developers use ngrok or similar tunneling tools. They work, but they have significant limitations:

❌ URLs Change on Restart

Every time you restart ngrok, you get a new URL. This means:

  • Updating webhook URLs in Stripe, GitHub, Shopify, etc. constantly
  • Breaking your development flow
  • Losing webhook history when URLs change

❌ Exposes Your Local Server

Your local development server is exposed to the internet through a tunnel. While ngrok has some security features, it's still not ideal.

❌ Dev-Only Tool

ngrok is great for development, but you can't use the same infrastructure for production. You'll need a different solution later.

❌ Limited Monitoring

You get basic request logs, but no comprehensive webhook delivery tracking, retry management, or debugging tools.

A Better Approach: Volley CLI

I built Volley to solve these problems. It's a webhook-as-a-service platform with a CLI tool for local development. Here's how it works:

How It Works

  1. Create a permanent webhook URL in Volley (never changes)
  2. Configure your webhook provider (Stripe, GitHub, etc.) to send to that URL
  3. Use Volley CLI to forward webhooks to your local server
  4. Same URL works in production - no code changes needed

Key Advantages

Permanent URLs - Never change, even when you restart

No Tunneling - Your local server stays completely private

Production Ready - Same infrastructure for dev and prod

Built-in Monitoring - Full webhook delivery dashboard

Automatic Retries - Handles failures gracefully

Works Offline - Webhooks are queued if your server is down

Quick Start

Installation

# macOS
brew tap volleyhq/volley
brew install volley

# Linux
wget https://github.com/volleyhq/volley-cli/releases/latest/download/volley-linux-amd64.tar.gz
tar -xzf volley-linux-amd64.tar.gz
sudo mv volley /usr/local/bin/
Enter fullscreen mode Exit fullscreen mode

Setup

  1. Create a free Volley account (10K events/month free tier)
  2. Create a webhook source in the dashboard
  3. Copy your ingestion ID (e.g., abc123xyz)
  4. Configure your provider to send webhooks to: https://api.volleyhooks.com/hook/YOUR_INGESTION_ID

Example: Node.js/Express

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

app.use(express.json());

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

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

Forward webhooks to localhost:

volley listen --source abc123xyz --forward-to http://localhost:3000/webhook
Enter fullscreen mode Exit fullscreen mode

That's it! Now any webhook sent to your Volley URL will be forwarded to your local server.

Example: Python/Flask

# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    print(f'Webhook received: {data}')
    # Your webhook handling logic here
    return jsonify({'received': True})

if __name__ == '__main__':
    app.run(port=3000)
Enter fullscreen mode Exit fullscreen mode

Forward webhooks:

volley listen --source abc123xyz --forward-to http://localhost:3000/webhook
Enter fullscreen mode Exit fullscreen mode

Example: Go

// main.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    var data map[string]interface{}
    json.NewDecoder(r.Body).Decode(&data)

    fmt.Printf("Webhook received: %+v\n", data)
    // Your webhook handling logic here

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    log.Println("Server running on http://localhost:3000")
    log.Fatal(http.ListenAndServe(":3000", nil))
}
Enter fullscreen mode Exit fullscreen mode

Run:

go run main.go
volley listen --source abc123xyz --forward-to http://localhost:3000/webhook
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Multiple Destinations

You can forward the same webhook source to multiple local endpoints:

# Terminal 1: Main API
volley listen --source abc123xyz --forward-to http://localhost:3000/webhook

# Terminal 2: Webhook processor
volley listen --source abc123xyz --forward-to http://localhost:3001/process

# Terminal 3: Logging service
volley listen --source abc123xyz --forward-to http://localhost:3002/log
Enter fullscreen mode Exit fullscreen mode

Production Deployment

When you're ready for production, just point your Volley source to your production endpoint. No code changes needed - the same webhook URL works everywhere.

Comparison: Volley vs ngrok

Feature Volley ngrok
Webhook URLs ✅ Permanent, never change ❌ Change on restart
Tunneling ❌ Not required ✅ Required
Local Server Privacy ✅ Completely private ⚠️ Exposed through tunnel
Built-in Retry ✅ Automatic ❌ No
Monitoring ✅ Full dashboard ❌ Limited
Production Ready ✅ Same URL for dev/prod ❌ Dev tool only
Offline Support ✅ Webhooks queued ❌ Must be online

Real-World Example: Stripe Webhooks

Let's say you're building a payment integration with Stripe:

  1. Create Volley source → Get permanent URL: https://api.volleyhooks.com/hook/abc123xyz
  2. Configure Stripe → Add webhook URL in Stripe Dashboard
  3. Develop locally → Use volley listen to forward to localhost:3000
  4. Deploy to production → Point Volley source to https://your-api.com/webhooks
  5. Same URL, zero code changes → Stripe keeps sending to the same Volley URL

Security Best Practices

When testing webhooks locally, always:

  1. Verify webhook signatures (when available)
  2. Use environment variables for secrets
  3. Handle idempotency to prevent duplicate processing
// Example: Stripe webhook verification
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

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,
      process.env.STRIPE_WEBHOOK_SECRET
    );

    // Handle the event
    console.log('Verified webhook:', event.type);
    res.json({received: true});
  } catch (err) {
    console.log('Webhook signature verification failed:', err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
});
Enter fullscreen mode Exit fullscreen mode

Getting Started

Ready to try it? Here's everything you need:

Why I Built This

After years of dealing with webhook testing frustrations, I decided to build something better. Volley started as a side project to solve my own problems, but I realized other developers were facing the same issues.

The goal was simple: make webhook development as smooth as possible, from local testing to production deployment. No URL changes, no tunneling, no separate infrastructure for dev and prod.

What's Next?

I'm constantly improving Volley based on developer feedback. Upcoming features:

  • More webhook provider integrations
  • Enhanced monitoring and debugging tools
  • Webhook replay functionality
  • Team collaboration features

Have questions or feedback? Drop a comment below or check out the GitHub repository.


Happy webhook testing! 🚀

P.S. - If you're working with webhook signature verification across multiple providers, I'm working on a comprehensive guide. Stay tuned!

Top comments (0)