Guide to Cloudflare Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Alerts > Destinations > Webhooks > Create, or Terraform's cloudflare_ |
| Subscription model | Notification policies, each with one alert type, send to chosen destinations |
| Limits | Available on all plans. Up to 100 webhook destinations for accounts without a paid zone |
| Authentication | Optional secret, sent as-is in cf-webhook-auth |
| Signature | None |
| Event field | alert_type in the body, which may be absent |
| Target restrictions | Publicly resolvable address on port 80 or 443 |
| Source IPs | Cloudflare's published IP ranges |
| Retries, timeout, ordering | Not documented |
Common events
Cloudflare's payload reference gives examples for these alert types:
alert_type | Fires when |
|---|---|
advanced_ | A Layer 3/4 DDoS attack is detected |
advanced_ | A Layer 7 DDoS attack is detected |
health_ | An origin health check changes status |
dedicated_ | A dedicated or custom certificate event occurs, such as expiry |
access_ | An Access custom certificate is close to expiry |
workers_alert | A Workers usage alert fires |
workers_ | A 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
- In the Cloudflare dashboard, go to Alerts > Destinations.
- In the Webhooks card, select Create and give the webhook a name.
- Enter your URL. It must resolve to a public address on port 80 or 443.
- 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 - Select Save and Test. Cloudflare sends the test message to your URL.
- 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-authsecret, 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
2xxquickly. - 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-relayexample. - 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.