Webhook monitoring

Monitor a webhook receiver with an uptime check on a GET route at the same path, because providers only ever POST to it, and monitor the jobs behind it with a push monitor they call like an inbound webhook, so a missing call becomes an alert.

A webhook receiver fails quietly. Stripe retries a failed delivery for up to three days in live mode, so a broken endpoint does not look like an outage. It looks like nothing. You find out when a customer paid and their account never upgraded.

The obvious fix does not work as is. Logdash monitors send a plain GET with no body and no custom headers, and a receiver built for a provider only accepts POST. Point a monitor at /webhooks/stripe and it gets a 404 or a 405 on every check and stays red forever. So give the monitor its own door on the same path.

Webhook endpoint monitoring

routes/webhooks.js
import express from 'express';
import { pool } from '../db.js';
import { handleStripeEvent } from '../stripe.js';

export const webhooks = express.Router();

// What Stripe calls. Raw body, because the signature check needs it.
webhooks.post(
  '/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  handleStripeEvent,
);

// What the monitor calls. Same path, same process, same database.
webhooks.get('/webhooks/stripe', async (req, res) => {
  try {
    await pool.query('select 1');
    res.status(200).send('ok');
  } catch {
    res.status(503).send('db');
  }
});

Same path on purpose. A router refactor that drops the route, a proxy rule that stops forwarding it, a deploy that never came up: each turns this GET red the moment the POST starts failing. What it cannot see is a rotated signing secret or a provider that stopped sending, because the GET never runs your signature check. The provider's delivery log covers those.

Cron webhook monitoring

Push monitors flip the direction. Your code calls POST https://api.logdash.io/ping/<monitorId> with no auth header and no body, and the monitor goes down when a check window passes without a call. Push monitors are on Pro, where the window is 15 seconds. That fits a worker or a queue consumer that loops all day. It does not fit a crontab line that runs hourly or nightly, because the monitor would go down 15 seconds after every run. For those, Healthchecks.io or Cronitor, with a cron expression and a grace period per job, are the better tool.

worker-loop.sh
# The ping fires only when a pass succeeds, so a crash or a hang
# reads as a missed window. Keep one pass plus the sleep under 15 s.
while true; do
  /srv/app/bin/drain-queue \
    && curl -fsS -m 5 -X POST https://api.logdash.io/ping/68b4c1f0e3a2d5c7b9f01234
  sleep 10
done
  1. Add the GET route Ship the GET handler beside the POST handler and open https://yourapp.com/webhooks/stripe in a browser. You should see ok, not a 405.
  2. Create the monitors Add a monitor with that URL. It checks every 5 minutes on the free plan, every minute on Builder and every 15 seconds on Pro. A service holds one monitor, so on Pro add a second service with a push monitor for the worker and paste its id into the loop.
  3. Break it on purpose Connect Telegram, then stop the database. The GET returns 503, the monitor flips to down on the next check, and a Telegram alert names the receiver and shows the 503.

Webhook monitoring tools

An uptime check and a webhook gateway answer different questions. Logdash asks your receiver whether it can take a request. Hookdeck sits in front of it and sees every real delivery.

Logdash vs Hookdeck

FeatureLogdashHookdeck
Receiver down during a quiet hourCaught on the next check, every 15 seconds on ProSeen when the next real event fails to deliver
One failed deliveryInvisible while the GET still answers 200Opens an issue with the payload attached
RetriesNone, the provider retries on its own scheduleAutomatic retries, plus manual and bulk retries
Alert channelsTelegram and webhook, and that is the listEmail and webhook, Slack and PagerDuty on paid plans
Change to your stackOne GET routeEvery provider points at a Hookdeck URL, one more hop
Free tier5 monitors, checked every 5 minutes10,000 events a month, 3-day retention

When Hookdeck is the better pick

  • Every event matters. A GET that answers 200 tells you nothing about the one invoice.paid that failed at 02:14.
  • You want failed events retried after the fix, not only an alert that something broke.
  • You want the alert in Slack or PagerDuty without writing and hosting the bridge yourself.

Webhook failure alert

Logdash alerts on the transition, once when a monitor goes down and once when it recovers, not on every failed check. Besides Telegram, the webhook channel calls your own URL. On the free plan it sends a GET with no body. On Builder and Pro, set it to POST to receive JSON with httpMonitorId, newStatus, name, url, errorMessage and statusCode.

What are the best webhook monitoring tools?
It depends on the question you need answered. Hookdeck sits in the request path, so it sees every delivery and every failure. Logdash checks that the receiver can take a request at all, every 15 seconds on Pro, and adds push monitors for the workers behind it. If each event carries money, the gateway is the one to pick first.
How does cron webhook monitoring work?
The job calls a URL when it succeeds, and the monitor alerts when the call does not arrive. In Logdash that is a push monitor on Pro: a POST to the monitor's ping URL inside every 15-second window. Jobs that run hourly or nightly fit Healthchecks.io or Cronitor better.
How do I set up webhook endpoint monitoring when the endpoint only accepts POST?
Add a GET handler on the same path that runs one database query and returns 200 or 503, then point an uptime monitor at it. Logdash monitors only send GET, without a body or headers, so they cannot replay a signed POST.
How do I get a webhook failure alert?
For the receiver, an uptime monitor on its GET route sends a Telegram alert when it returns 503 or does not answer within 10 seconds. For a single failed event, read the provider's delivery log or put a gateway such as Hookdeck in front, because an uptime check never sees individual deliveries.

Point it at your own URL and watch it for real.