DEV Community

Ozor
Ozor

Posted on

How to Build a Webhook Delivery Monitor with JavaScript (Free APIs)

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:

  1. Catches webhooks on any path (e.g., /hooks/stripe, /hooks/github)
  2. Logs the sender's IP, geo data, headers, and payload
  3. Validates the origin against expected IP ranges
  4. 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}`));
Enter fullscreen mode Exit fullscreen mode

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 }));
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Running It

export FROSTBYTE_KEY="your-api-key"
export SLACK_WEBHOOK="https://hooks.slack.com/services/..." # optional
node webhook-monitor.js
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)