Testing Webhooks with ngrok and smee
In this tutorial, you will learn about Testing Webhooks with ngrok and smee. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn webhook testing: use ngrok and smee.io to test webhooks locally, inspect webhook requests, replay webhooks, set up testing workflows, and write automated webhook tests.
What You Learn
You will learn how to test webhooks locally using ngrok and smee.io, inspect incoming webhook requests with inspectors, replay webhooks for debugging, write automated tests for webhook handlers, and set up a webhook testing workflow.
Why It Matters
Webhook providers need a public URL to send events. During development, your server runs on localhost. Without tools like ngrok, you cannot test webhook integration until deployment. Testing locally catches issues early and saves deployment cycles.
Real-World Use
DodaTech developers use ngrok for daily webhook testing. Each developer has a persistent ngrok endpoint. Stripe and GitHub webhooks are forwarded to local servers. The webhook inspector captures requests for debugging and replay.
Setting Up ngrok
# Install ngrok
npm install -g ngrok
# Start ngrok tunnel to local webhook server
ngrok http 3000
# Output:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000
Expected output: ngrok creates a public HTTPS URL that forwards to your local webhook server. Configure your webhook provider to send to https://abc123.ngrok.io/webhooks.
ngrok Configuration
# ngrok.yml - Persistent configuration
version: "2"
authtoken: your_ngrok_auth_token
tunnels:
webhook-server:
proto: http
addr: 3000
inspect: true # Enable request inspection
domain: my-dev.ngrok.io # Custom domain (paid)
basic_auth:
- "webhook:test123" # Protect your tunnel
webhook-secondary:
proto: http
addr: 3001
inspect: true
Expected output: Persistent ngrok configuration with fixed subdomain, basic authentication to prevent unwanted requests, and request inspection enabled for debugging.
Webhook Inspector
// Using ngrok's inspection API to read captured requests
class NgrokWebhookInspector {
constructor(ngrokApiKey) {
this.apiKey = ngrokApiKey;
this.baseUrl = 'https://api.ngrok.com';
}
async getRecentRequests(tunnelName, limit = 10) {
const response = await fetch(
`${this.baseUrl}/tunnel_sessions/${tunnelName}/requests?limit=${limit}`,
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Ngrok-Version': '2',
},
}
);
const data = await response.json();
return data.requests.map(req => ({
id: req.id,
method: req.method,
path: req.path,
statusCode: req.response.status_code,
duration: req.response.duration_ms,
requestHeaders: req.request.headers,
requestBody: req.request.body,
responseHeaders: req.response.headers,
responseBody: req.response.body,
timestamp: req.started_at,
}));
}
async replayRequest(requestId) {
const request = await this.getRequestDetail(requestId);
// Replay the request to your local server
const response = await fetch('http://localhost:3000' + request.path, {
method: request.method,
headers: request.requestHeaders,
body: request.requestBody,
});
return {
replayed: true,
originalId: requestId,
statusCode: response.status,
replayedAt: new Date().toISOString(),
};
}
async getRequestDetail(requestId) {
const response = await fetch(
`${this.baseUrl}/requests/${requestId}`,
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Ngrok-Version': '2',
},
}
);
return response.json();
}
}
Expected output: ngrok's inspection API captures every webhook request. Developers can view request details, replay failed webhooks, and debug integration issues without the provider resending.
Using smee.io
// smee.io - lightweight webhook forwarding for development
const SmeeClient = require('smee-client');
const smee = new SmeeClient({
source: 'https://smee.io/your-channel-id',
target: 'http://localhost:3000/webhooks',
headers: {
'X-Forwarded-For': 'smee.io',
},
});
const events = smee.start();
// Listen for events
events.on('message', (message) => {
console.log('Received webhook via smee:', {
path: message.path,
method: message.method,
headers: message.headers,
body: message.body?.slice(0, 200),
});
});
events.on('error', (err) => {
console.error('smee error:', err.message);
});
Expected output: smee.io creates a public URL (https://smee.io/your-channel-id). Providers send webhooks to this URL. smee forwards them to your local server. No installation or auth required.
Automated Webhook Tests
const crypto = require('crypto');
const express = require('express');
// Test helper for webhook handlers
class WebhookTestHelper {
constructor(secret) {
this.secret = secret;
}
createSignedPayload(payload) {
const body = JSON.stringify(payload);
const signature = crypto
.createHmac('sha256', this.secret)
.update(body)
.digest('hex');
return {
body,
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `sha256=${signature}`,
'X-Webhook-Timestamp': new Date().toISOString(),
'X-Webhook-ID': `test_${Date.now()}`,
},
};
}
async sendTestWebhook(app, payload) {
const { body, headers } = this.createSignedPayload(payload);
const response = await fetch('http://localhost:3000/webhooks/test', {
method: 'POST',
headers,
body,
});
return {
status: response.status,
headers: response.headers,
body: await response.text(),
};
}
}
// Automated test example
async function testWebhookHandler() {
const helper = new WebhookTestHelper('test_secret_123');
// Test valid webhook
const validResult = await helper.sendTestWebhook({
event: 'payment.succeeded',
data: { id: 'pi_test_123', amount: 5000 },
});
console.assert(validResult.status === 200, 'Valid webhook should return 200');
// Test invalid signature
const invalidPayload = helper.createSignedPayload({ test: true });
invalidPayload.headers['X-Webhook-Signature'] = 'sha256=invalid';
const invalidResult = await fetch('http://localhost:3000/webhooks/test', {
method: 'POST',
headers: invalidPayload.headers,
body: invalidPayload.body,
});
console.assert(invalidResult.status === 401, 'Invalid signature should return 401');
// Test replay (stale timestamp)
const stalePayload = helper.createSignedPayload({ test: true });
stalePayload.headers['X-Webhook-Timestamp'] =
new Date(Date.now() - 600000).toISOString();
const staleResult = await fetch('http://localhost:3000/webhooks/test', {
method: 'POST',
headers: stalePayload.headers,
body: stalePayload.body,
});
console.assert(staleResult.status === 400, 'Stale webhook should return 400');
console.log('All tests passed!');
}
Expected output: Automated tests verify webhook handler behavior: valid webhooks return 200, invalid signatures return 401, stale webhooks return 400. Tests run in CI/CD pipeline.
Integration Testing with Providers
// Provider-specific test helpers
class ProviderTestClient {
// Stripe test webhook
static async sendStripeTestWebhook(eventType, data) {
const stripe = require('stripe')(process.env.STRIPE_TEST_KEY);
const event = await stripe.testHelpers.testHelpers
.testClock.advance('clock_id');
// Use Stripe's test webhook endpoint
const response = await fetch(
'https://api.stripe.com/v1/webhook_endpoints/test_helpers/send',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STRIPE_TEST_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
'event_type': eventType,
}),
}
);
return response;
}
// GitHub test webhook
static async sendGitHubTestWebhook(eventType, payload) {
const response = await fetch(
'https://api.github.com/repos/owner/repo/dispatches',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/vnd.github.v3+json',
},
body: JSON.stringify({
event_type: eventType,
client_payload: payload,
}),
}
);
return response;
}
}
Expected output: Provider test helpers send real test webhooks using provider sandbox environments. Stripe test mode and GitHub Repository dispatches generate genuine webhook payloads for integration testing.
Common Mistakes
1. Testing Only with Fake Payloads
Fake payloads may not match real provider payload structure. Always test with real payloads from provider documentation or sandbox. Capture and replay real webhooks for realistic testing.
2. Exposing Localhost Without Auth
ngrok tunnels are public. Anyone with the URL can send requests. Use basic auth in ngrok config or add authentication to your webhook endpoint during development.
3. Not Testing Error Scenarios
Testing only the happy path misses signature failures, timeouts, malformed JSON, and provider downtime. Write tests for: invalid signatures, expired timestamps, missing fields, oversized payloads.
4. Manual Testing Only
Relying on manual ngrok testing does not scale. Write automated tests that verify webhook handling. Run them in CI/CD. Use replay for Regression Testing.
5. No Webhook Playback Tests
After fixing a bug, replay the failed webhook to verify the fix. Capture the original request from ngrok inspector or your delivery log. Replay it against the fixed code.
Practice Questions
1. How does ngrok enable local webhook testing?
ngrok creates a public HTTPS tunnel to your local server. Providers send webhooks to the ngrok URL, which forwards them to localhost. The ngrok dashboard captures and displays all requests.
2. What is the difference between ngrok and smee.io?
ngrok is a full tunneling solution with request inspection, replay, and paid features (custom domains, reserved tunnels). smee.io is a simple, free webhook forwarding service without inspection or replay.
3. How do you replay a webhook for debugging?
Use ngrok's inspection API to get the raw request. Send the same request to your local server using curl or a test script. This reproduces the exact scenario without the provider resending.
4. What should automated webhook tests verify?
Signature verification (valid and invalid), timestamp validation (current and stale), payload validation (valid and malformed), idempotency (duplicate detection), and event type routing (known and unknown events).
Challenge
Build a webhook testing framework with: ngrok tunnel for local testing, automated test suite with signed payload generation, provider-specific test helpers (Stripe, GitHub), webhook replay capability from captured requests, CI/CD integration that runs tests against a test provider endpoint, and coverage reporting for webhook handler code.
FAQ
Mini Project: Webhook Testing CLI
Build a CLI tool for webhook testing: send test webhooks with configurable payloads and signatures, replay captured webhooks from ngrok or log files, verify webhook endpoint responses, run test suites against webhook handlers, and generate test coverage reports.
What's Next
Now that you can test webhooks, explore Svix and Standard Webhooks for managed webhook delivery platforms.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro