Gareth Wilson Gareth Wilson

Guide to Cloudflare Webhooks: Features and Best Practices

Published


Cloudflare Notifications can alert you about DDoS attacks, origin errors, failing health checks, expiring certificates and Workers problems. Slack, PagerDuty and similar services have built-in destinations, but a generic webhook lets you send the same alerts to your own incident tooling, a SIEM or an automation that reacts to them.

This guide covers how Cloudflare generic webhook destinations and notification policies fit together, the alert payload, how to check Cloudflare's cf-webhook-auth secret, and what Cloudflare leaves undocumented about delivery.

What are Cloudflare webhooks?

Cloudflare alerts are configured in two parts. A destination is a webhook URL with an optional secret, created once per account. A notification policy picks an alert type, such as an HTTP DDoS attack or a health check status change, and sends it to one or more destinations.

Each alert arrives as a JSON POST. Cloudflare doesn't sign the body. Instead, if you set a secret on the destination, Cloudflare sends it unchanged in the cf-webhook-auth header of every request, and tells you to reject requests where the header is missing or doesn't match.

Cloudflare webhook features

FeatureDetails
ConfigurationAlerts > Destinations > Webhooks > Create, or Terraform's cloudflare_notification_policy_webhooks
Subscription modelNotification policies, each with one alert type, send to chosen destinations
LimitsAvailable on all plans. Up to 100 webhook destinations for accounts without a paid zone
AuthenticationOptional secret, sent as-is in cf-webhook-auth
SignatureNone
Event fieldalert_type in the body, which may be absent
Target restrictionsPublicly resolvable address on port 80 or 443
Source IPsCloudflare's published IP ranges
Retries, timeout, orderingNot documented

Common events

Cloudflare's payload reference gives examples for these alert types:

alert_typeFires when
advanced_ddos_attack_l4_alertA Layer 3/4 DDoS attack is detected
advanced_ddos_attack_l7_alertA Layer 7 DDoS attack is detected
health_check_status_notificationAn origin health check changes status
dedicated_ssl_certificate_event_typeA dedicated or custom certificate event occurs, such as expiry
access_custom_certificate_expiration_typeAn Access custom certificate is close to expiry
workers_alertA Workers usage alert fires
workers_observability_alertA Workers observability alert fires

Cloudflare has many more notification types, and its field reference also names http_alert_origin_error as an example. Each uses the same envelope with its own alert_type and data:

{
  "name": "string",
  "text": "string",
  "data": {},
  "ts": 1136214245,
  "account_id": "string",
  "policy_id": "string",
  "policy_name": "string",
  "alert_type": "string",
  "alert_correlation_id": "string",
  "alert_event": "string"
}

text is a readable summary with values filled in, and data holds the alert-specific fields. ts is the Unix time the notification was generated. alert_event is ALERT_STATE_EVENT_START or ALERT_STATE_EVENT_END for alerts that open and close, and alert_correlation_id groups related alerts so you can match an end to its start.

Cloudflare notes that account_id, policy_id and alert_type may be missing, depending on the alert. The Save and Test message is the extreme case: its body is only {"text": "Hello World! This is a test message sent from https://cloudflare.com. ..."}. Write handlers that fall back to text when alert_type is missing.

Setting up Cloudflare webhooks

  1. In the Cloudflare dashboard, go to Alerts > Destinations.
  2. In the Webhooks card, select Create and give the webhook a name.
  3. Enter your URL. It must resolve to a public address on port 80 or 443.
  4. Enter a Secret. For a generic webhook, you choose the value. Use a long random string and store it, for example as CLOUDFLARE_WEBHOOK_SECRET.
  5. Select Save and Test. Cloudflare sends the test message to your URL.
  6. Go to the notifications list, add or edit a notification, and select this webhook as its destination.

If your endpoint sits on a private network or a different port, Cloudflare suggests forwarding alerts with a Worker or routing to it through Cloudflare Tunnel.

Securing Cloudflare webhooks

Compare the cf-webhook-auth header with your stored secret in constant time, and reject the request if the header is missing:

const crypto = require('crypto');
const express = require('express');

function verifyCloudflareSecret(headerValue, secret) {
  if (!headerValue || !secret) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(secret, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/cloudflare', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.CLOUDFLARE_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyCloudflareSecret(req.get('cf-webhook-auth'), secret)) return res.sendStatus(401);

  const alert = JSON.parse(req.body.toString('utf8'));
  // Route on alert.alert_type (may be missing), then process async.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os


def verify_cloudflare_secret(header_value, secret: str) -> bool:
    if not header_value or not secret:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), secret.encode("utf-8"))


# In FastAPI:
# verify_cloudflare_secret(request.headers.get("cf-webhook-auth"), os.environ["CLOUDFLARE_WEBHOOK_SECRET"])

Because the secret travels in every request and nothing covers the body, it only proves the request came from something that knows the secret. Use HTTPS so it can't be read in transit.

Make Cloudflare alerts production-ready. Hookdeck Event Gateway checks the cf-webhook-auth secret, stores every alert, and retries delivery to your tooling, which Cloudflare doesn't document doing itself.

In Hookdeck, create a Cloudflare source and enter the same secret. Hookdeck compares it with cf-webhook-auth on each request. The secret is optional in Cloudflare, so leave verification off if you didn't set a secret on the destination.

Cloudflare webhook limitations and pain points

A shared secret, not a signature

The Problem: The secret proves who sent the request, but not that the body is unchanged.

Why It Happens: Cloudflare sends the secret itself in cf-webhook-auth rather than an HMAC of the payload, and there is no timestamp.

Workarounds:

  • Use HTTPS only, and don't log the header.
  • Allowlist Cloudflare's IP ranges as a second check, keeping in mind that the ranges are shared across Cloudflare services and change over time.

How Hookdeck Can Help: Only Hookdeck needs the Cloudflare secret. Your services receive alerts from Hookdeck and don't have to store or compare it.

No documented retries

The Problem: An alert sent while your endpoint is down may be lost, and you won't know.

Why It Happens: Cloudflare documents no retry policy, timeout, ordering or delivery ID for generic webhooks.

Workarounds:

  • Keep the receiver small and highly available, and return 2xx quickly.
  • Send critical alert types to a second destination, such as email or PagerDuty.

How Hookdeck Can Help: Hookdeck accepts and stores each alert, then retries delivery to your service on a schedule you set, so an outage on your side doesn't drop it.

Inconsistent payloads

The Problem: Handlers that assume alert_type or account_id crash on some alerts.

Why It Happens: Cloudflare marks those fields as optional, and the test message has only text.

Workarounds:

  • Check for each field before using it, and fall back to text.
  • Log unknown shapes rather than returning an error.

How Hookdeck Can Help: Hookdeck filters can route on alert_type when it is present, and send everything else to a catch-all destination.

Public ports only

The Problem: Internal alert handlers can't receive Cloudflare webhooks directly.

Why It Happens: Generic webhooks only go to publicly resolvable addresses on port 80 or 443.

Workarounds:

  • Forward alerts with a Worker, as in Cloudflare's cf-webhook-relay example.
  • Expose the handler through Cloudflare Tunnel.

How Hookdeck Can Help: Point Cloudflare at a public Hookdeck URL. For local development, the Hookdeck CLI forwards events to a port on your machine.

Best practices

Always set a secret

Cloudflare recommends it for generic webhooks, and it is the only authentication on offer.

Pair start and end events

Use alert_correlation_id with alert_event to open an incident on ALERT_STATE_EVENT_START and close it on ALERT_STATE_EVENT_END.

Use ts for timing

ts is when Cloudflare generated the notification. Use it to order alerts and to spot delays.

Split policies by audience

Each notification policy can send to different destinations. Send security alerts and Workers alerts to the teams that act on them rather than one shared endpoint.

Conclusion

Cloudflare generic webhooks carry notification alerts in a common envelope, authenticated by a secret you choose and Cloudflare sends back in cf-webhook-auth. Set the secret, compare it in constant time, handle missing fields, and plan for the retries Cloudflare doesn't document.

Hookdeck Event Gateway stores every alert and retries delivery so your incident tooling doesn't miss one.

Get started with Hookdeck for free and handle Cloudflare webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.