Complete guide to testing webhooks locally with Volley - Works with any framework, any language, any webhook provider.
This repository demonstrates how to test webhooks locally using Volley with various frameworks and languages. No ngrok, no tunneling, no exposing your server.
- How to set up Volley for local webhook testing
- How to use Volley CLI to forward webhooks
- Examples in multiple languages and frameworks
- Best practices for local webhook development
- Volley account (free tier: 10K events/month)
- Volley CLI installed
- Your preferred development environment
-
Install Volley CLI:
# 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/
-
Login to Volley:
volley login
-
Create a webhook source:
- Go to Volley Dashboard
- Create a new source
- Copy your ingestion ID (e.g.,
abc123xyz)
-
Configure your webhook provider:
- Add your Volley webhook URL:
https://api.volleyhooks.com/hook/YOUR_INGESTION_ID - Configure in Stripe, GitHub, Twilio, etc.
- Add your Volley webhook URL:
-
Start your local server (see examples below)
-
Forward webhooks to localhost:
volley listen --source YOUR_INGESTION_ID --forward-to http://localhost:3000/webhook
// 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');
});Forward webhooks:
volley listen --source abc123xyz --forward-to http://localhost:3000/webhook# 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)Forward webhooks:
volley listen --source abc123xyz --forward-to http://localhost:3000/webhook# main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class WebhookData(BaseModel):
event: str
data: dict
@app.post("/webhook")
async def webhook(data: WebhookData):
print(f'Webhook received: {data}')
# Your webhook handling logic here
return {"received": True}Run:
uvicorn main:app --port 3000
volley listen --source abc123xyz --forward-to http://localhost:3000/webhook// 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))
}Run:
go run main.go
volley listen --source abc123xyz --forward-to http://localhost:3000/webhook# app.rb
require 'sinatra'
require 'json'
post '/webhook' do
data = JSON.parse(request.body.read)
puts "Webhook received: #{data}"
# Your webhook handling logic here
{ received: true }.to_json
endRun:
ruby app.rb
volley listen --source abc123xyz --forward-to http://localhost:3000/webhookYou 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| Feature | Volley | ngrok |
|---|---|---|
| Webhook URLs | β Permanent, never change | β Change on restart |
| Tunneling | β Not required | β Required |
| Local Server Privacy | β Completely private | |
| 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 |
Learn more: Volley vs ngrok
Always verify webhook signatures when available:
// Example: Stripe webhook verification
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);Never commit secrets:
# .env
WEBHOOK_SECRET=your_secret_hereHandle duplicate webhooks:
const eventId = req.body.id;
if (await isEventProcessed(eventId)) {
return; // Already processed
}
await markEventAsProcessed(eventId);- Trigger a webhook from your provider (Stripe, GitHub, etc.)
- Watch it appear in your local server logs
- Check Volley dashboard for delivery status
// test/webhook.test.js
const request = require('supertest');
const app = require('../server');
test('handles webhook', async () => {
const response = await request(app)
.post('/webhook')
.send({ event: 'test', data: {} });
expect(response.status).toBe(200);
});- Volley Documentation
- Volley CLI Documentation
- Testing Stripe Webhooks Locally
- Ngrok Alternative Guide
Found a bug or want to add an example? Please open an issue or submit a pull request!
MIT License - See LICENSE file for details
Built with β€οΈ using Volley