A webhook 403 Forbidden response means the request reached your server but was rejected before it ever got to your handler. Almost always: a layer between the public internet and your handler — Cloudflare, AWS WAF, an nginx allowlist, your firewall — decided this request shouldn't be processed.
Root Causes
1. WAF or bot-protection rules
Cloudflare's "Under Attack" mode, AWS WAF managed rules, and Akamai's bot manager all flag webhook deliveries as automated traffic — which they technically are. Your handler never runs because the WAF returned 403 first. The fix is a WAF bypass rule for the webhook URL: match the path (and ideally also the source IP range from the provider's published list) and skip the WAF challenge.
2. IP allowlist mismatches
You configured nginx or AWS Security Groups to only accept traffic from "known" IPs. The provider rotated their outbound IPs (Stripe, GitHub, Shopify all do this regularly) and your allowlist is now stale. Check the provider's docs for the latest IP ranges and either auto-update them or remove the allowlist (signature verification is the real defense, not IP filtering).
3. Cloudflare "Browser Integrity Check"
This Cloudflare feature returns 403 to clients without certain browser-like headers (Cookie, Accept-Language). Webhook senders are HTTP libraries — they don't send those headers. Disable Browser Integrity Check on the webhook URL pattern, or whitelist the provider's IP range.
4. Missing or wrong auth token
If your endpoint requires an API key (custom auth setup, not signature verification), and the provider isn't sending it because you forgot to configure the static header in their dashboard — every delivery returns 403. Check the provider's webhook config for "custom headers" and confirm they're being sent.
Fix It
# Cloudflare — bypass WAF on webhook routes
# Rules → WAF → Custom rules → Add rule
# When: URI Path equals /webhook OR /webhook/*
# Then: Skip → All managed rules
# Optional safeguard: AND ip.src in {provider's published CIDRs}
# nginx — selectively disable a strict ModSecurity rule
location /webhook/stripe {
modsecurity_rules '
SecRuleRemoveById 949110
';
proxy_pass http://upstream;
}
Provider-Specific Notes
- Stripe: stripe.com/docs/ips publishes the egress CIDRs.
- GitHub: see
https://api.github.com/metafor thehooksfield. - Shopify: does not publish stable IPs; allowlist is unreliable for Shopify. Use signature verification instead.
- Twilio: documents IPs per region.
How to Reproduce
Send a test request from your local machine and from a known cloud IP using curl -v. If local works but cloud doesn't, the IP allowlist is the cause. If neither works, the WAF or auth layer is. Compare response headers — Cloudflare emits cf-ray on every block, AWS WAF emits x-amzn-trace-id.
Frequently Asked Questions
Should I rely on IP allowlists for webhook security?
No. Provider IPs change without notice and allowlists are brittle. Signature verification (HMAC) is the only reliable webhook authentication. Use IP allowlists only as defense-in-depth, never as the sole control.
Why does Cloudflare return 403 for my legitimate webhook?
Cloudflare's bot detection treats any non-browser user-agent as suspicious by default. The fix is a path-specific bypass rule — keep WAF on the rest of your site, skip it for the webhook routes.
Can I just set the WAF security level to 'low'?
You can, but it weakens protection across your whole site. Better to add a targeted bypass rule that scopes the bypass to webhook paths only.