Webhook Tester Skill
Test webhook integrations locally with tunneling, inspection, and debugging tools.
Instructions
You are a webhook testing expert. When invoked:
-
Local Webhook Testing:
- Set up local webhook receivers
- Expose localhost to internet using tunnels
- Capture and inspect webhook payloads
- Verify webhook signatures
- Test retry mechanisms
-
Debugging Webhooks:
- Inspect request headers and body
- Validate webhook signatures
- Test different payload formats
- Simulate webhook failures
- Log and replay webhooks
-
Integration Testing:
- Test webhook delivery
- Verify idempotency
- Test retry logic
- Validate error handling
- Performance testing
-
Security Validation:
- Verify signature validation
- Test HTTPS requirements
- Validate origin checking
- Test replay attack prevention
Usage Examples
@webhook-tester
@webhook-tester --setup-tunnel
@webhook-tester --inspect
@webhook-tester --verify-signature
@webhook-tester --replay
Tunneling Tools
ngrok (Most Popular)
Basic Setup
# Install ngrok
# Download from https://ngrok.com/download
# Or use package manager
brew install ngrok/ngrok/ngrok # macOS
choco install ngrok # Windows
# Authenticate (get token from ngrok.com)
ngrok config add-authtoken YOUR_TOKEN
# Start tunnel to localhost:3000
ngrok http 3000
# Custom subdomain (requires paid plan)
ngrok http 3000 --subdomain=myapp
# Multiple ports
ngrok http 3000 3001
# Use specific region
ngrok http 3000 --region=us
# Enable inspection UI
ngrok http 3000 --inspect=true
ngrok Configuration File
# ~/.ngrok2/ngrok.yml
version: "2"
authtoken: YOUR_TOKEN
tunnels:
api:
addr: 3000
proto: http
subdomain: myapi
webhooks:
addr: 4000
proto: http
subdomain: webhooks
web:
addr: 8080
proto: http
bind_tls: true
# Start all tunnels
ngrok start --all
# Start specific tunnel
ngrok start api
ngrok API
// Using ngrok programmatically
const ngrok = require('ngrok');
async function startTunnel() {
const url = await ngrok.connect({
addr: 3000,
region: 'us',
onStatusChange: status => console.log('Status:', status)
});
console.log('Tunnel URL:', url);
// Use this URL as webhook endpoint
return url;
}
// Cleanup
async function stopTunnel() {
await ngrok.disconnect();
await ngrok.kill();
}
Cloudflare Tunnel (Free, No Account Required)
# Install
brew install cloudflare/cloudflare/cloudflared # macOS
# Or download from cloudflare.com
# Quick tunnel (no auth required)
cloudflared tunnel --url http://localhost:3000
# Output will be: https://random-words.trycloudflare.com
localtunnel
# Install
npm install -g localtunnel
# Start tunnel
lt --port 3000
# Custom subdomain (may not be available)
lt --port 3000 --subdomain myapp
# Use localtunnel programmatically
const localtunnel = require('localtunnel');
const tunnel = await localtunnel({ port: 3000 });
console.log('Tunnel URL:', tunnel.url);
tunnel.on('close', () => {
console.log('Tunnel closed');
});
VS Code Port Forwarding
# In VS Code with GitHub account
# 1. Open Terminal
# 2. Click "Ports" tab
# 3. Click "Forward a Port"
# 4. Enter port number (e.g., 3000)
# 5. Share the public URL
Webhook Receiver Setup
Express.js Webhook Endpoint
const express = require('express');
const crypto = require('crypto');
const app = express();
// Raw body parser for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
// Webhook endpoint
app.post('/webhooks/github', (req, res) => {
console.log('Received webhook from GitHub');
console.log('Headers:', req.headers);
console.log('Body:', req.body);
// Verify signature
const signature = req.headers['x-hub-signature-256'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyGitHubSignature(req.rawBody, signature, secret)) {
console.error('Invalid signature');
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = req.headers['x-github-event'];
handleGitHubEvent(event, req.body);
// Always respond quickly (GitHub expects response within 10s)
res.status(200).send('OK');
});
function verifyGitHubSignature(payload, signature, secret) {
if (!signature) return false;
const hmac = crypto.createHmac('sha256', secret);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
function handleGitHubEvent(event, payload) {
switch (event) {
case 'push':
console.log('Push event:', payload.ref);
break;
case 'pull_request':
console.log('PR event:', payload.action);
break;
default:
console.log('Unhandled event:', event);
}
}
// Stripe webhook
app.post('/webhooks/stripe', (req, res) => {
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent succeeded:', paymentIntent.id);
break;
case 'payment_intent.failed':
console.log('PaymentIntent failed');
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
res.json({ received: true });
});
// Generic webhook logger
app.post('/webhooks/:service', (req, res) => {
const { service } = req.params;
console.log(`\n${'='.repeat(50)}`);
console.log(`Webhook received: ${service}`);
console.log(`Timestamp: ${new Date().toISOString()}`);
console.log(`${'='.repeat(50)}`);
console.log('\nHeaders:');
Object.entries(req.headers).forEach(([key, value]) => {
console.log(` ${key}: ${value}`);
});
console.log('\nBody:');
console.log(JSON.stringify(req.body, null, 2));
console.log(`${'='.repeat(50)}\n`);
res.status(200).json({ received: true });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook receiver listening on port ${PORT}`);
});
Python Flask Webhook Receiver
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
app = Flask(__name__)
@app.route('/webhooks/github', methods=['POST'])
def github_webhook():
# Verify signature
signature = request.headers.get('X-Hub-Signature-256')
secret = os.getenv('WEBHOOK_SECRET')
if not verify_github_signature(request.data, signature, secret):
return 'Invalid signature', 401
event = request.headers.get('X-GitHub-Event')
payload = request.json
print(f'Received {event} event')
print(f'Payload: {payload}')
# Process event
handle_github_event(event, payload)
return 'OK', 200
def verify_github_signature(payload, signature, secret):
if not signature:
return False
mac = hmac.new(
secret.encode(),
msg=payload,
digestmod=hashlib.sha256
)
expected = 'sha256=' + mac.hexdigest()
return hmac.compare_digest(expected, signature)
def handle_github_event(event, payload):
if event == 'push':
print(f"Push to {payload['ref']}")
elif event == 'pull_request':
print(f"PR {payload['action']}")
@app.route('/webhooks/<service>', methods=['POST'])
def generic_webhook(service):
print(f'\n{"=" * 50}')
print(f'Webhook received: {service}')
print(f'{"=" * 50}')
print('\nHeaders:')
for key, value in reque