Table of Contents
What Are Webhooks?
Webhooks are HTTP callbacks — automated messages sent from one application to another when a specific event occurs. Instead of polling an API repeatedly to check for changes, webhooks push data to your endpoint in real-time.
Common webhook providers include:
- Stripe — payment events (charge succeeded, subscription canceled)
- GitHub — repository events (push, pull request, issue created)
- Slack — message events, slash commands
- Shopify — order events (created, fulfilled, refunded)
- Twilio — SMS and call events
Why Test Webhooks?
Webhook testing is critical because:
- You can't control the sender — the webhook provider sends data at unpredictable times
- Payloads vary — different events have different structures
- Failures are silent — if your endpoint fails, you might not know until a customer complains
- Security matters — you need to verify signatures to prevent spoofing
3 Methods to Test Webhooks
Method 1: Online Webhook Testers
The fastest way to test webhooks is with an online tool that gives you a unique URL to receive requests. Our free Webhook Tester does exactly this:
- Create a new webhook bin (unique URL)
- Configure your webhook provider to send to that URL
- Trigger an event and inspect the captured request
# Create a webhook bin via API
curl -X POST https://kas.storksoft.by/api/v1/webhook/create
# Send a test webhook
curl -X POST https://kas.storksoft.by/api/v1/webhook/catch/YOUR_BIN_ID \
-H "Content-Type: application/json" \
-d '{"event": "payment.succeeded", "amount": 4999}'
# Inspect captured requests
curl https://kas.storksoft.by/api/v1/webhook/inspect/YOUR_BIN_ID
Try Our Free Webhook Tester
No signup required. Create a URL, send requests, inspect everything.
Open Webhook Tester →Method 2: Local Testing with Tunnels
When developing locally, your localhost isn't accessible from the internet. Use a tunnel service to expose your local server:
Using ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js # listening on port 3000
# Create a tunnel
ngrok http 3000
# Use the generated URL (e.g., https://abc123.ngrok.io)
# as your webhook endpoint
Using Cloudflare Tunnel
# Install cloudflared
brew install cloudflare/cloudflare/cloudflared
# Quick tunnel (no account needed)
cloudflared tunnel --url http://localhost:3000
Method 3: Mock Testing in CI/CD
For automated tests, simulate webhook deliveries with mock HTTP requests:
Node.js Example (Jest + Supertest)
const request = require('supertest');
const app = require('../app');
describe('POST /webhooks/stripe', () => {
it('handles payment_intent.succeeded', async () => {
const payload = {
type: 'payment_intent.succeeded',
data: {
object: { id: 'pi_123', amount: 4999, currency: 'usd' }
}
};
const res = await request(app)
.post('/webhooks/stripe')
.set('Content-Type', 'application/json')
.set('Stripe-Signature', generateTestSignature(payload))
.send(payload);
expect(res.status).toBe(200);
// Verify side effects (e.g., order created, email sent)
});
});
Python Example (pytest + FastAPI)
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
def test_github_webhook():
payload = {
"action": "opened",
"pull_request": {"number": 42, "title": "Fix bug"}
}
response = client.post(
"/webhooks/github",
json=payload,
headers={"X-GitHub-Event": "pull_request"}
)
assert response.status_code == 200
Debugging Common Webhook Issues
1. 401/403 — Authentication Failed
Your endpoint is rejecting the request. Check that you're verifying the webhook signature correctly. Most providers include a signature in a header (e.g., Stripe-Signature, X-Hub-Signature-256).
2. 500 — Server Error
Your handler code is crashing. Common causes:
- Missing required fields in the payload
- Database connection issues
- Unhandled event types
3. Timeout
Webhook providers typically wait 5-30 seconds for a response. If your processing takes longer, return 200 OK immediately and process the event asynchronously (e.g., with a message queue).
4. Duplicate Events
Webhook providers may retry failed deliveries, leading to duplicates. Always make your webhook handler idempotent — processing the same event twice should have the same result as processing it once.
// Idempotent webhook handler pattern
async function handleWebhook(event) {
// Check if we've already processed this event
const existing = await db.events.findOne({ eventId: event.id });
if (existing) return; // Already processed, skip
// Process the event
await processEvent(event);
// Mark as processed
await db.events.insert({ eventId: event.id, processedAt: new Date() });
}
Webhook Security Best Practices
- Verify signatures — Always check the HMAC signature sent by the provider
- Use HTTPS — Never accept webhooks over plain HTTP
- Validate payloads — Check the structure matches what you expect
- Rate limit — Protect against flood attacks on your webhook endpoint
- Log everything — Store raw payloads for debugging and auditing
Stripe Signature Verification Example
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.WEBHOOK_SECRET);
} catch (err) {
console.log(`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':
// Handle successful payment
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
res.json({received: true});
});
Ready to Test Your Webhooks?
Our free tool captures and displays every detail of incoming requests.
Start Testing →