Webhooks are the backbone of modern integrations — Stripe sends payment events, GitHub sends push notifications, Slack sends message events. But when a webhook fails silently, you lose data and don't know why.
In this tutorial, we'll build a webhook delivery monitor that:
- Accepts incoming webhooks from any service
- Logs every delivery with full headers and body
- Verifies the sender's IP and geolocation
- Sends alerts when deliveries fail or come from unexpected origins
All using free APIs. No database. Under 80 lines of code.
What We're Building
A lightweight Node.js server that:
- Catches webhooks on any path (e.g.,
/hooks/stripe,/hooks/github) - Logs the sender's IP, geo data, headers, and payload
- Validates the origin against expected IP ranges
- Sends a Slack/Discord alert if something looks suspicious
Prerequisites
- Node.js 18+
- A free API key from Frostbyte (200 free credits)
Step 1: Set Up the Server
import http from 'node:http';
const API_KEY = process.env.FROSTBYTE_KEY || 'your-api-key';
const ALERT_WEBHOOK = process.env.SLACK_WEBHOOK; // Optional Slack/Discord webhook
const PORT = process.env.PORT || 4000;
// Store recent deliveries in memory
const deliveries = [];
const MAX_DELIVERIES = 1000;
const server = http.createServer(async (req, res) => {
if (req.method === 'GET' && req.url === '/') {
// Dashboard endpoint
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
total: deliveries.length,
recent: deliveries.slice(-20).reverse()
}));
}
if (req.method === 'POST') {
await handleWebhook(req, res);
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(PORT, () => console.log(`Webhook monitor on :${PORT}`));
Step 2: Handle Incoming Webhooks
async function handleWebhook(req, res) {
const startTime = Date.now();
const ip = req.headers['x-forwarded-for']?.split(',')[0] ||
req.socket.remoteAddress;
// Read the body
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = Buffer.concat(chunks).toString();
// Look up sender's IP geolocation
const geo = await lookupIP(ip);
const delivery = {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
path: req.url,
method: req.method,
ip,
geo: geo ? {
country: geo.country,
city: geo.city,
org: geo.org,
isp: geo.isp
} : null,
headers: Object.fromEntries(
Object.entries(req.headers).filter(([k]) =>
!['host', 'connection', 'accept-encoding'].includes(k)
)
),
bodySize: body.length,
bodyPreview: body.slice(0, 500),
latencyMs: Date.now() - startTime
};
deliveries.push(delivery);
if (deliveries.length > MAX_DELIVERIES) deliveries.shift();
// Check for suspicious origins
if (geo && !isExpectedOrigin(req.url, geo)) {
await sendAlert(delivery);
}
console.log(`[${delivery.timestamp}] ${delivery.path} from ${ip} (${geo?.country || 'unknown'})`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: true, id: delivery.id }));
}
Step 3: IP Geolocation Lookup
This is where the free API comes in. Every webhook delivery gets an IP lookup so you know where it's coming from:
async function lookupIP(ip) {
// Skip private/local IPs
if (ip === '127.0.0.1' || ip === '::1' || ip?.startsWith('192.168.')) {
return null;
}
try {
const res = await fetch(
`https://api.frostbyte.world/ip/geo/${ip}`,
{ headers: { 'x-api-key': API_KEY } }
);
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
The API returns:
{
"ip": "185.199.108.153",
"country": "United States",
"city": "San Francisco",
"region": "California",
"timezone": "America/Los_Angeles",
"org": "AS36459 GitHub, Inc.",
"isp": "GitHub, Inc.",
"lat": 37.7749,
"lon": -122.4194
}
Step 4: Origin Validation
Define expected IP ranges for each webhook source. Flag anything unexpected:
const EXPECTED_ORIGINS = {
'/hooks/stripe': {
countries: ['US', 'IE'],
orgs: ['Stripe']
},
'/hooks/github': {
countries: ['US'],
orgs: ['GitHub']
}
};
function isExpectedOrigin(path, geo) {
const expected = EXPECTED_ORIGINS[path];
if (!expected) return true; // No rules = allow all
const countryOk = expected.countries.some(c =>
geo.country?.toLowerCase().includes(c.toLowerCase())
);
const orgOk = expected.orgs.some(o =>
geo.org?.toLowerCase().includes(o.toLowerCase()) ||
geo.isp?.toLowerCase().includes(o.toLowerCase())
);
return countryOk || orgOk;
}
Step 5: Alert on Suspicious Deliveries
Send a notification when a webhook comes from an unexpected origin:
async function sendAlert(delivery) {
const message = [
`Suspicious webhook delivery`,
`Path: ${delivery.path}`,
`IP: ${delivery.ip}`,
`Location: ${delivery.geo?.city}, ${delivery.geo?.country}`,
`Org: ${delivery.geo?.org}`,
`Body size: ${delivery.bodySize} bytes`
].join('\n');
console.warn(`[ALERT] ${message}`);
if (ALERT_WEBHOOK) {
try {
await fetch(ALERT_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
} catch (err) {
console.error('Failed to send alert:', err.message);
}
}
}
Running It
export FROSTBYTE_KEY="your-api-key"
export SLACK_WEBHOOK="https://hooks.slack.com/services/..." # optional
node webhook-monitor.js
Test with curl:
# Simulate a Stripe webhook
curl -X POST http://localhost:4000/hooks/stripe \
-H "Content-Type: application/json" \
-H "Stripe-Signature: test_sig_123" \
-d '{"type": "payment_intent.succeeded", "data": {"amount": 2000}}'
# Check the dashboard
curl http://localhost:4000/
What You Get
{
"total": 1,
"recent": [
{
"id": "a1b2c3d4-...",
"timestamp": "2026-03-05T20:15:30.000Z",
"path": "/hooks/stripe",
"ip": "185.199.108.153",
"geo": {
"country": "United States",
"city": "San Francisco",
"org": "AS36459 GitHub, Inc."
},
"headers": {
"content-type": "application/json",
"stripe-signature": "test_sig_123"
},
"bodySize": 58,
"latencyMs": 45
}
]
}
Taking It Further
Some ideas to extend this:
- Signature verification: Validate HMAC signatures for Stripe, GitHub, etc.
- Retry tracking: Store delivery IDs and detect duplicates
- Rate limiting: Alert if a source sends too many hooks per minute
- SQLite persistence: Replace the in-memory array with a database
- DNS lookup: Use a free DNS API to resolve the sender's hostname
Get Your Free API Key
The IP geolocation lookups use Frostbyte's free API. You get 200 credits with no credit card — each lookup costs 1 credit.
curl -X POST https://api.frostbyte.world/api/keys/create
Works with 40+ API endpoints: IP geolocation, DNS resolution, website screenshots, crypto prices, code execution, and more.
What webhook integrations are you monitoring? Drop a comment below.
Top comments (0)