There are two kinds of webhook, and they fail in opposite directions. Outbound webhook monitoring — the subject of a separate guide — watches the endpoints your app calls when it sends events out. This guide is about the other half: the inbound webhook receivers, the endpoints inside your own app that external services like Stripe, GitHub, Twilio, Shopify, and Slack POST events into.

Inbound failures are the more dangerous kind because they're silent. When your receiver starts returning 500s or timing out, the sending service doesn't email you — it just retries on its own schedule, then eventually gives up and drops the event. Your dashboards show no errors because, from your app's perspective, nothing happened. The first sign is indirect and delayed: payments stop reconciling, deploys stop triggering, SMS replies never get processed. By the time you notice, you may have lost hours of events with no easy way to replay them. This guide shows how to monitor inbound receivers with CronAlert so a broken handler pages you in minutes.

Why inbound receiver failures are so easy to miss

A webhook receiver is a route in your app, but it has a few properties that make it uniquely prone to silent failure:

  • No human is watching it. Nobody loads the Stripe webhook URL in a browser. There's no user to complain when it breaks, so it can be down for hours before anyone notices a downstream symptom.
  • The sender retries quietly. Providers retry failed deliveries (Stripe for up to three days, GitHub a few times, etc.), which masks the problem — until the retry window closes and the event is gone for good.
  • It depends on more than HTTP. A receiver typically verifies a signature, writes to a database, and enqueues a job. Any of those dependencies can break while the route itself still responds.
  • It's a deploy-time casualty. Webhook routes break on the boring stuff: a changed path, a rotated signing secret that didn't get updated, a new required middleware, a body parser that now consumes the raw payload signature verification needs.

This is the same "the front door is fine, the thing behind it is broken" problem that motivates a database health endpoint — except here the broken thing is the receiver itself, and the only "user" is a machine that won't tell you it's failing.

Don't monitor by sending real events

The naive approach is to have your uptime monitor POST a fake event to the real webhook handler every minute. Don't. Real handlers:

  • Verify a cryptographic signature. A synthetic payload without a valid signature is correctly rejected (a 400), which looks like a failure even though the endpoint is healthy.
  • Have side effects. A fake payment_succeeded event might create a duplicate record, send a customer email, or kick off fulfillment.
  • Aren't idempotent against arbitrary input. Replaying the same synthetic event can corrupt state.

The right pattern, just like with any deep check in the HTTP health check endpoint guide, is a dedicated, side-effect-free health route that exercises the receiver's real dependencies and returns a clean 200/503.

Build a receiver health route

Add a health route next to each webhook handler — for example /webhooks/stripe/health alongside /webhooks/stripe. It should check everything the real handler needs to succeed, without doing the handler's work:

  • The signing secret is present and non-empty. A missing or empty STRIPE_WEBHOOK_SECRET after a deploy is one of the most common silent breakages — the route is up but rejects every real event.
  • The database is reachable. The handler writes the event; if the DB is down, the receiver can't persist.
  • The queue is reachable (if you enqueue work, which you should — see below).
const express = require('express');
const app = express();

app.get('/webhooks/stripe/health', async (req, res) => {
  if (req.headers['x-health-token'] !== process.env.HEALTH_TOKEN) {
    return res.status(401).json({ status: 'unauthorized' });
  }

  const problems = [];
  if (!process.env.STRIPE_WEBHOOK_SECRET) problems.push('missing_signing_secret');

  try {
    await db.query('SELECT 1');
  } catch {
    problems.push('database_unreachable');
  }

  try {
    await queue.ping();
  } catch {
    problems.push('queue_unreachable');
  }

  const healthy = problems.length === 0;
  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'healthy' : 'unhealthy',
    problems,
  });
});

Protect the route with a token (as above) so you're not publicly advertising the internal state of your payment plumbing, and never leak raw error text — the same two rules from the database health endpoint guidance.

Monitor freshness, not just availability

A health route proves the receiver could process an event. It doesn't prove events are actually arriving. For a high-volume source — Stripe on a busy store, GitHub on an active repo — a sudden silence is itself a signal that something upstream (the sender, DNS, a firewall rule, a misconfigured endpoint in their dashboard) is wrong, even though your endpoint is perfectly healthy.

The fix is the dead-man's-switch / heartbeat pattern applied to inbound events:

  • In your receiver, update a last_event_received_at timestamp every time a valid event lands.
  • Expose that timestamp through a small endpoint (or have a job ping a CronAlert heartbeat URL whenever an event arrives).
  • Alert if no event has arrived within the expected window — say, 30 minutes for a source that normally fires several times an hour.

This catches the failure mode availability monitoring can't: the endpoint is up, returns 200, and yet zero events are coming through because the sender disabled the endpoint after too many past failures, or someone deleted it from the provider dashboard.

Respond fast, process async

A subtle receiver failure is being too slow. Stripe, GitHub, and most providers expect a response within a few seconds and treat a timeout as a failed delivery — so they retry, and now you're processing the same event multiple times. The fix is to verify the signature, enqueue the work, and return 2xx immediately, doing the heavy lifting in a background worker.

That means there are two things to monitor: that the receiver returns 2xx, and that it responds fast. CronAlert records response time on every check, so you can catch a receiver that's creeping toward the sender's timeout before it tips over into retry storms and duplicate processing. The receiver and the worker are a pair: monitor the receiver for "did we accept the event" and the worker for "did we actually process it."

Setting it up in CronAlert

  1. Create a monitor per receiver health route — one for /webhooks/stripe/health, one for /webhooks/github/health, and so on. Add the x-health-token as a custom header. Separate monitors mean the alert names the exact integration that's broken.
  2. Expect 200, so a missing signing secret or unreachable database (503) fires the alert.
  3. Add keyword monitoring (Pro) to require "healthy" in the body, catching a proxy that rewrites the 503 into a 200. See keyword monitoring.
  4. Add a heartbeat monitor for high-volume sources to detect a drop to zero events, per the heartbeat pattern.
  5. Watch response time and set the interval to 1 minute on a paid plan — a slow receiver causes duplicate processing, not just delay.
  6. Route by impact. A broken payment webhook is revenue-critical; route it to PagerDuty or Opsgenie. A broken Slack-notification webhook can be a lower-priority channel. See incident response workflows.

Common pitfalls

  • Monitoring the route but not its dependencies. A receiver that returns 200 but can't reach the database is broken. The health route must exercise the real dependencies.
  • POSTing fake events to the live handler. Signature verification rejects them and side effects corrupt state. Use a dedicated health route.
  • Forgetting the signing secret after a deploy. The single most common silent break — the route is up, but every real event fails verification. Check the secret's presence in the health route.
  • Processing inline and timing out. Heavy work before returning 2xx causes the sender to retry and you to double-process. Enqueue and acknowledge fast; monitor response time.
  • No freshness check. Availability monitoring won't tell you the sender stopped sending. Add a heartbeat for high-volume sources.
  • One monitor for all receivers. If a single check covers every integration, the alert can't tell you which one broke. One monitor per receiver.

Where this fits in a broader strategy

Inbound receiver monitoring complements its mirror image, outbound webhook reliability monitoring, and sits alongside third-party dependency monitoring (the services sending you events are dependencies too), background worker monitoring (the receiver hands off to the worker), and database health monitoring (the receiver writes there). Together they trace the full path of an inbound event from "the sender POSTed it" through "we accepted it" to "we actually processed it." For the bigger picture, see uptime monitoring for SaaS.

Frequently asked questions

What is the difference between monitoring inbound and outbound webhooks?

Outbound monitoring watches the endpoints your app POSTs events to. Inbound (receiver) monitoring watches the endpoints in your own app that external services POST into. Inbound failures are sneakier — the sender retries quietly and your dashboards show no error, so you only notice when payments stop reconciling or deploys stop firing.

How do you monitor a webhook receiver endpoint?

Add a side-effect-free health route next to the handler (e.g. /webhooks/stripe/health) that checks the same dependencies the real handler needs — signing secret, database, queue — and returns 200 only when the receiver could actually process an event. Point CronAlert at that route, combine the status-code check with keyword monitoring, and route failures to on-call.

Why can't I just send a test webhook to monitor it?

Because real handlers verify a signature, have side effects, and aren't idempotent against arbitrary payloads. A synthetic event every minute would fail verification or create duplicate records. Expose a dedicated health route that checks dependencies without triggering side effects, and monitor that instead.

How do you detect missing or delayed webhooks?

Availability monitoring proves the endpoint is up, not that events are arriving. Use a heartbeat / dead-man's-switch: update a "last event received" timestamp in the receiver and alert if no event arrives within the expected window for a high-volume source.

What status code should a webhook receiver return?

A healthy receiver acknowledges with a 2xx (200 or 202) after verifying the signature and enqueuing the work — not after fully processing it. That tells the sender to stop retrying. Doing heavy work inline risks exceeding the sender's timeout and triggering retries, so monitor both the status code and the response time.

Monitor your webhook receivers with CronAlert

Inbound webhooks fail silently, and silent failures are the expensive kind — lost payments, missed deploys, dropped messages discovered hours later. Create a free account (25 monitors, no credit card), add a health route next to each receiver, point a monitor at it with keyword monitoring on Pro, add a heartbeat for your high-volume sources, and route the payment webhook to your pager. The next time a deploy blanks your signing secret or your database hiccups, you'll hear it from CronAlert — not from a customer asking why their payment never went through.

Related reading: monitoring outbound webhook endpoints for reliability, cron job and heartbeat monitoring, monitoring background workers and queue consumers, monitoring third-party dependencies, and the complete guide to HTTP health check endpoints.