Test webhook integrations locally with tunneling, inspection, and debugging tools.
Sets up local webhook testing with tunneling, payload inspection, and signature verification. Use when you need to debug webhook integrations locally or test provider-specific security requirements.
/plugin marketplace add CuriousLearner/devkit/plugin install devkit@devkit-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Test webhook integrations locally with tunneling, inspection, and debugging tools.
You are a webhook testing expert. When invoked:
Local Webhook Testing:
Debugging Webhooks:
Integration Testing:
Security Validation:
@webhook-tester
@webhook-tester --setup-tunnel
@webhook-tester --inspect
@webhook-tester --verify-signature
@webhook-tester --replay
# 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
# ~/.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
// 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();
}
# 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
# 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');
});
# 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
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}`);
});
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 request.headers:
print(f' {key}: {value}')
print('\nBody:')
print(request.get_data(as_text=True))
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
# 1. Visit https://webhook.site
# 2. Get unique URL (e.g., https://webhook.site/abc-123)
# 3. Use this URL as webhook endpoint
# 4. View all incoming requests in real-time
# Features:
# - Unique URL per session
# - View request headers and body
# - Custom response configuration
# - Request history
# - Share URL with team
// 1. Create Mock Server in Postman
// 2. Add webhook endpoint
// 3. Configure response
// 4. Use mock URL as webhook endpoint
// Example Mock Server Response
{
"statusCode": 200,
"body": {
"received": true,
"timestamp": "{{$timestamp}}"
}
}
// webhook-cli.js
const express = require('express');
const chalk = require('chalk');
class WebhookTester {
constructor(port = 3000) {
this.app = express();
this.port = port;
this.requests = [];
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
this.app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
}
setupRoutes() {
// Catch all webhook requests
this.app.all('/webhooks/*', (req, res) => {
const webhook = {
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
headers: req.headers,
body: req.body,
query: req.query
};
this.requests.push(webhook);
this.logWebhook(webhook);
res.status(200).json({ received: true });
});
}
logWebhook(webhook) {
console.log(chalk.blue('\n' + '='.repeat(60)));
console.log(chalk.green('Webhook Received'));
console.log(chalk.blue('='.repeat(60)));
console.log(chalk.yellow('\nTimestamp:'), webhook.timestamp);
console.log(chalk.yellow('Method:'), webhook.method);
console.log(chalk.yellow('Path:'), webhook.path);
console.log(chalk.yellow('\nHeaders:'));
Object.entries(webhook.headers).forEach(([key, value]) => {
console.log(` ${chalk.gray(key)}: ${value}`);
});
if (Object.keys(webhook.query).length > 0) {
console.log(chalk.yellow('\nQuery:'));
console.log(JSON.stringify(webhook.query, null, 2));
}
console.log(chalk.yellow('\nBody:'));
console.log(JSON.stringify(webhook.body, null, 2));
console.log(chalk.blue('='.repeat(60) + '\n'));
}
start() {
this.app.listen(this.port, () => {
console.log(chalk.green(`\nWebhook tester running on http://localhost:${this.port}`));
console.log(chalk.gray('Waiting for webhooks...\n'));
});
}
getHistory() {
return this.requests;
}
clearHistory() {
this.requests = [];
console.log(chalk.yellow('History cleared'));
}
}
// Usage
const tester = new WebhookTester(3000);
tester.start();
const crypto = require('crypto');
function verifyGitHubWebhook(payload, signature, secret) {
if (!signature || !signature.startsWith('sha256=')) {
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)
);
}
// Test
const payload = JSON.stringify({ test: 'data' });
const secret = 'my-webhook-secret';
const signature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
console.log('Valid:', verifyGitHubWebhook(payload, signature, secret));
const stripe = require('stripe')('sk_test_...');
app.post('/webhooks/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
webhookSecret
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Process the event
console.log('Event:', event.type);
res.json({ received: true });
});
const crypto = require('crypto');
function verifyShopifyWebhook(body, hmacHeader, secret) {
const hash = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(hmacHeader)
);
}
app.post('/webhooks/shopify', (req, res) => {
const hmac = req.headers['x-shopify-hmac-sha256'];
const secret = process.env.SHOPIFY_SECRET;
if (!verifyShopifyWebhook(req.rawBody, hmac, secret)) {
return res.status(401).send('Invalid signature');
}
res.status(200).send('OK');
});
const request = require('supertest');
const app = require('./app');
const crypto = require('crypto');
describe('Webhook Tests', () => {
const webhookSecret = 'test-secret';
function generateSignature(payload) {
return 'sha256=' + crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(payload))
.digest('hex');
}
describe('POST /webhooks/github', () => {
test('should accept valid webhook', async () => {
const payload = {
ref: 'refs/heads/main',
commits: []
};
const signature = generateSignature(payload);
const response = await request(app)
.post('/webhooks/github')
.set('X-Hub-Signature-256', signature)
.set('X-GitHub-Event', 'push')
.send(payload);
expect(response.status).toBe(200);
});
test('should reject invalid signature', async () => {
const payload = { test: 'data' };
const response = await request(app)
.post('/webhooks/github')
.set('X-Hub-Signature-256', 'invalid')
.set('X-GitHub-Event', 'push')
.send(payload);
expect(response.status).toBe(401);
});
test('should reject missing signature', async () => {
const payload = { test: 'data' };
const response = await request(app)
.post('/webhooks/github')
.set('X-GitHub-Event', 'push')
.send(payload);
expect(response.status).toBe(401);
});
});
});
const fs = require('fs').promises;
const path = require('path');
class WebhookStorage {
constructor(storageDir = './webhooks') {
this.storageDir = storageDir;
}
async saveWebhook(webhook) {
const filename = `${Date.now()}-${webhook.path.replace(/\//g, '-')}.json`;
const filepath = path.join(this.storageDir, filename);
await fs.mkdir(this.storageDir, { recursive: true });
await fs.writeFile(filepath, JSON.stringify(webhook, null, 2));
console.log('Webhook saved:', filepath);
}
async loadWebhook(filename) {
const filepath = path.join(this.storageDir, filename);
const content = await fs.readFile(filepath, 'utf8');
return JSON.parse(content);
}
async replayWebhook(filename) {
const webhook = await this.loadWebhook(filename);
const response = await fetch(`http://localhost:3000${webhook.path}`, {
method: webhook.method,
headers: webhook.headers,
body: JSON.stringify(webhook.body)
});
console.log('Replayed webhook:', filename);
console.log('Response:', response.status);
}
}
app.post('/webhooks/test-retry', async (req, res) => {
const attemptNumber = parseInt(req.headers['x-attempt'] || '1');
const maxAttempts = 3;
console.log(`Attempt ${attemptNumber}/${maxAttempts}`);
// Fail first 2 attempts
if (attemptNumber < maxAttempts) {
console.log('Simulating failure');
return res.status(500).send('Temporary error');
}
console.log('Success on final attempt');
res.status(200).send('OK');
});
// Retry logic (sender side)
async function sendWebhookWithRetry(url, payload, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Attempt': attempt.toString()
},
body: JSON.stringify(payload)
});
if (response.ok) {
console.log(`Webhook delivered on attempt ${attempt}`);
return response;
}
console.log(`Attempt ${attempt} failed: ${response.status}`);
} catch (error) {
console.log(`Attempt ${attempt} error:`, error.message);
}
// Exponential backoff
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`Waiting ${delay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('All webhook delivery attempts failed');
}
Signature Header: X-Hub-Signature-256
Event Header: X-GitHub-Event
Algorithm: HMAC SHA-256
Signature Header: Stripe-Signature
Algorithm: HMAC SHA-256 (special format)
Test Mode: Use Stripe CLI
Signature Header: X-Shopify-Hmac-SHA256
Algorithm: HMAC SHA-256 (base64)
Topic Header: X-Shopify-Topic
Signature Header: X-Twilio-Signature
Algorithm: HMAC SHA-1
Validation: Special URL + params
Signature Header: X-Slack-Signature
Timestamp Header: X-Slack-Request-Timestamp
Algorithm: HMAC SHA-256
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.