ScreenshotNeo

BlogEngineering

Webhook Payloads for Website Monitoring Alerts

Learn how to parse, validate, secure, deduplicate, and route website-monitoring webhook alerts, recoveries, and test events across providers.

By the ScreenshotNeo team29 September 20269 min read

Webhook Payloads for Website Monitoring Alerts

A website-monitoring webhook payload is a JSON document sent by a monitoring service to your HTTP endpoint when a check opens an alert, recovers, or runs a test. There is no universal payload standard: every provider chooses its own envelope, field names, status values, authentication, and retry behavior.

Build your receiver around a small, stable core: verify the request, parse JSON, identify the event type, record the provider event ID and raw body, deduplicate, then enqueue slow work. Keep provider-specific adapters at the boundary so the rest of your system receives one normalized event shape.

What a monitoring webhook contains

Most payloads have an envelope plus alert-specific data. The envelope commonly identifies the event, monitor or policy, timestamp, and correlation key. The nested data describes the check result, incident, resource, or content change.

Concept Typical fields Why you need it
Event identity id, event_id, alert_correlation_id Deduplication and audit trails
Event kind type, alert_event, state Separate alert, recovery, and test handling
Time ts, started_at, ended_at Ordering, replay protection, and incident duration
Monitor identity monitor_id, policy_id, policy_name Routing and ownership
Check result status, duration_ms, error, region Diagnostics and severity decisions
Evidence summary, observed_value, history URL Useful notifications and investigation links

Do not assume optional identifiers are always present. Cloudflare documents an envelope containing name, text, data, ts, account_id, policy_id, policy_name, alert_type, alert_correlation_id, and alert_event; it also notes that some of these fields can be absent depending on notification context. See the Cloudflare webhook payload documentation.

Representative payload shapes

Alert, recovery, and test events

A provider such as PathWatch uses a top-level type to distinguish alert, recovery, and test messages. Its documented check statuses include success, error, timeout, degraded, skipped, and runner_unavailable.

A resilient receiver verifies, normalizes, deduplicates, and queues every webhook event.
A resilient receiver verifies, normalizes, deduplicates, and queues every webhook event.
{
  "type": "alert",
  "event_id": "evt_01J...",
  "monitor": {"id": "mon_123", "name": "Checkout"},
  "rule": {"name": "HTTP status is not 200", "threshold": 2},
  "check": {
    "status": "timeout",
    "duration_ms": 30000,
    "error": "connection timed out",
    "region": "us-east"
  },
  "occurred_at": "2026-09-29T12:00:00Z"
}

Cloud monitoring incidents

Google Cloud Monitoring webhook schema 1.2 places the incident in an incident object. It includes an incident ID, renotification flag, open or closed state, start and end times, summary, observed value, resource and metric identity, policy, condition, and documentation. Error Reporting notifications use schema 1.0, so check the top-level version before decoding.

{
  "version": "1.2",
  "incident": {
    "incident_id": "123456789",
    "renotify": false,
    "state": "open",
    "started_at": 1769774400,
    "ended_at": 0,
    "summary": "HTTP latency above threshold",
    "observed_value": "4.2",
    "resource": {"type": "uptime_url", "labels": {"host": "example.com"}},
    "metric": {"type": "custom.googleapis.com/http/latency"},
    "policy_name": "Checkout latency",
    "condition": {"display_name": "p95 > 2s"},
    "documentation": {"content": "Runbook: checkout"}
  }
}

Fastly sends custom webhook POST requests for both alert-fired and alert-resolved events, including an alert title and a history API link. Anakin documents signed website-change alerts with before and after content retrieval plus delivery and retry semantics. Treat these as separate adapters rather than trying to force every vendor into one raw schema.

Design a receiver that survives schema differences

  1. Read the raw bytes. Preserve the exact body for signature verification and audit. Parse JSON only after authentication checks when the provider signs the body.
  2. Verify authentication. Cloudflare documents the cf-webhook-auth header and says to reject missing or mismatched values. Other providers may use HMAC, a bearer token, or mutual TLS.
  3. Validate a tolerant schema. Require only fields you truly need. Accept optional nesting and unknown fields so additive provider changes do not break delivery.
  4. Classify the event. Map alert, recovery, test, and renotification to explicit internal event types.
  5. Deduplicate before side effects. Store provider name plus event ID, correlation ID, or a hash of the signed body. Apply a uniqueness constraint.
  6. Respond quickly. Return the provider’s expected 2xx response after durable enqueueing. Send notifications, screenshots, ticket updates, and enrichment asynchronously.
  7. Version your adapter. Keep the provider schema version and your normalized schema version in storage.

Normalized internal event

{
  "provider": "pathwatch",
  "provider_event_id": "evt_01J...",
  "kind": "alert",
  "monitor_id": "mon_123",
  "monitor_name": "Checkout",
  "status": "timeout",
  "occurred_at": "2026-09-29T12:00:00Z",
  "duration_ms": 30000,
  "region": "us-east",
  "message": "connection timed out",
  "raw": {"...": "original provider payload"}
}

Runnable Node.js receiver

This example uses only the Node.js standard library. It validates the content type, limits body size, stores event IDs in memory for demonstration, and handles alert, recovery, and test events. Replace the in-memory set with a database in production.

import http from 'node:http';

const seen = new Set();
const maxBytes = 1024 * 1024;

function readBody(req) {
  return new Promise((resolve, reject) => {
    let data = '';
    req.on('data', chunk => {
      data += chunk;
      if (Buffer.byteLength(data) > maxBytes) reject(new Error('payload too large'));
    });
    req.on('end', () => resolve(data));
    req.on('error', reject);
  });
}

const server = http.createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/monitoring-webhook') {
    res.writeHead(404).end(); return;
  }
  try {
    const raw = await readBody(req);
    const payload = JSON.parse(raw);
    const eventId = payload.event_id ?? payload.alert_correlation_id ?? payload.incident?.incident_id;
    if (!eventId) throw new Error('missing event identifier');
    if (seen.has(eventId)) { res.writeHead(204).end(); return; }
    seen.add(eventId);
    const kind = payload.type ?? payload.alert_event ?? payload.incident?.state ?? 'unknown';
    console.log(JSON.stringify({ eventId, kind, receivedAt: new Date().toISOString(), payload }));
    res.writeHead(204).end();
  } catch (err) {
    console.error(err.message);
    res.writeHead(400, {'content-type': 'application/json'}).end(JSON.stringify({error: err.message}));
  }
});
server.listen(8080, () => console.log('listening on :8080'));

Python parsing and validation

Use a schema library such as Pydantic when you need strict validation. The following standard-library example accepts common vendor variants while preserving unknown fields.

from datetime import datetime, timezone
from typing import Any


def normalize(provider: str, payload: dict[str, Any]) -> dict[str, Any]:
    incident = payload.get("incident") or {}
    event_id = (payload.get("event_id") or payload.get("id") or
                payload.get("alert_correlation_id") or incident.get("incident_id"))
    if not event_id:
        raise ValueError("missing event identifier")
    kind = payload.get("type") or payload.get("alert_event") or incident.get("state")
    if kind in {"open", "firing"}: kind = "alert"
    if kind in {"closed", "resolved"}: kind = "recovery"
    if kind not in {"alert", "recovery", "test", None}:
        kind = "unknown"
    check = payload.get("check") or {}
    return {
        "provider": provider,
        "provider_event_id": str(event_id),
        "kind": kind,
        "monitor_id": payload.get("monitor_id") or (payload.get("monitor") or {}).get("id"),
        "status": check.get("status") or payload.get("status"),
        "message": check.get("error") or payload.get("summary") or payload.get("text"),
        "received_at": datetime.now(timezone.utc).isoformat(),
        "raw": payload,
    }

if __name__ == "__main__":
    import json, sys
    print(json.dumps(normalize("example", json.load(sys.stdin)), indent=2))

Test a webhook with cURL

Send a representative event to a local or staging endpoint before connecting a production monitor.

curl -i -X POST http://localhost:8080/monitoring-webhook \
  -H 'content-type: application/json' \
  -d '{"type":"alert","event_id":"evt_demo_1","check":{"status":"timeout","error":"upstream timeout"}}'

Security, replay, and delivery behavior

  • Use HTTPS and reject unexpected methods, paths, content types, and oversized bodies.
  • Compare signatures with a constant-time function. Never log shared secrets or authorization headers.
  • Check timestamp freshness when the provider supplies a timestamp. Keep a replay window appropriate to its retry policy.
  • Persist the raw body, headers needed for verification, provider, event ID, and receipt time. Encrypt sensitive content and define retention.
  • Expect duplicate delivery. A successful HTTP response does not guarantee exactly-once processing.
  • Do not treat arrival order as event order. A recovery can arrive after a delayed alert; compare provider timestamps and state transitions.
  • Return a 2xx only after the event is durably queued. Return 4xx for invalid authentication or malformed permanent requests; use 5xx only when retry may succeed.

Google Cloud requires webhook endpoints to be publicly reachable over HTTP or HTTPS with a valid certificate; private endpoints need an intermediary such as Pub/Sub. Its console includes a test-connection action. PathWatch documents POST by default and PUT when configured. Confirm method, certificate, and retry rules for each provider.

Alert and recovery payloads are separate events connected by identity and time.
Alert and recovery payloads are separate events connected by identity and time.

Operational performance and cost considerations

Keep the synchronous path small: signature check, bounded JSON parse, deduplication lookup, durable enqueue, and response. Queue enrichment and notifications. Set connection and read timeouts, cap concurrency, and monitor queue age. A burst of renotifications should not exhaust database connections or notification quotas.

Payload size is usually modest, but content-change providers may include before and after bodies. Store large evidence in object storage and keep a reference in the event record. Redact secrets, cookies, authorization values, and personal data before sending payloads to chat or ticket systems.

Webhook delivery itself generally has no per-request cost stated in the cited schemas. Your costs come from compute, storage, queueing, outbound notifications, and any screenshot or browser work triggered by an event. Capture only on actionable events and deduplicate before starting expensive jobs.

Troubleshooting checklist

Symptom Likely cause Fix
401 or 403 Missing or incorrect signature/token Inspect exact headers, raw bytes, clock skew, and secret version.
400 malformed JSON Receiver read body incorrectly or provider sent a different content type Read bytes once, enforce UTF-8, log a redacted sample, and verify provider test delivery.
Repeated alerts Retries or renotifications Use a unique provider-event key and make handlers idempotent.
Recovery ignored Code handles only a firing/open value Map closed, resolved, and recovery variants explicitly.
Events arrive out of order Network or provider retry delay Compare event timestamps and maintain a state transition log.
Provider reports timeout Endpoint is private, certificate invalid, or response too slow Expose a valid HTTPS endpoint, queue quickly, and test from the provider console.
Parser breaks after provider update Strict decoding of optional or unknown fields Validate required fields, retain unknown fields, and branch on schema version.

Or skip the browser setup

If your monitoring workflow needs a screenshot of the failing page, ScreenshotNeo can capture it through one GET request. Its clean-shot pipeline accepts cookie and consent banners, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets you turn each step off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status.

See the ScreenshotNeo API documentation for all options. A webhook worker can call:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const buffer = Buffer.from(await res.arrayBuffer());

You can also use an MCP server so Claude, Cursor, or another MCP client can call take_screenshot, get_page_info, and capture_pdf. Features include full-page and element captures, device presets, dark mode, custom headers and cookies, waits, request blocking, caching, signed links, async jobs, bulk capture, and a usage API. 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Provider comparison questions

When evaluating a monitoring webhook, compare schema versioning, alert/recovery/test semantics, monitor and resource identifiers, diagnostic status values, timestamp format, authentication, retry and duplicate behavior, endpoint requirements, and links to incident history. Cloudflare, PathWatch, Google Cloud Monitoring, and Fastly document different choices across these axes, so your adapter boundary matters more than a single “universal” parser.

FAQ

Is there a standard website-monitoring webhook schema?

No. Webhooks are vendor-specific contracts. Normalize them internally and retain the original payload.

Should a recovery create a new incident?

Usually it closes or updates the existing incident identified by a correlation or incident ID. Keep the recovery event for audit.

Can a webhook endpoint be private?

Only when the provider offers a private delivery path. Google Cloud requires public HTTP or HTTPS reachability, so use an intermediary for private systems.

How do I handle a provider adding fields?

Ignore unknown fields, validate required fields, record the schema version, and alert on changes that affect your normalized mapping.

What should I capture for debugging?

Store provider, event ID, correlation ID, timestamp, verification result, response code, redacted headers, and the raw body under a defined retention policy.