Receiving Webhooks Through Cloudflare Tunnel

Expose a local webhook receiver through Cloudflare Tunnel with no open inbound ports, or harden a public one with proxied DNS.

Webhook providers only deliver to public HTTPS URLs. If your webhook receiver runs at home, behind a corporate firewall, or on a server where you do not want to open ports 80/443, Cloudflare Tunnel solves it: an outbound-only connector on your machine, TLS terminated by Cloudflare, no inbound firewall rules at all. This guide connects a receiver such as Core Webhook Module listening on localhost:8000 to a public hostname.

What you need

  • A free Cloudflare account with your domain added (its nameservers pointed at Cloudflare)
  • Your webhook receiver running locally on port 8000

Step 1: Create the tunnel

In the Cloudflare dashboard, go to the Zero Trust area, then Networking > Tunnels, and select Create a tunnel. Choose the cloudflared connector type and give the tunnel a name, for example webhooks.

Step 2: Install the connector

The dashboard shows an install command for your OS with the tunnel token embedded. On Debian/Ubuntu it has this shape:

curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
sudo cloudflared service install <YOUR_TUNNEL_TOKEN>

Copy the exact command from your dashboard; the token is unique to your tunnel. cloudflared runs as a service and only makes outbound connections to Cloudflare.

Step 3: Route a public hostname to your receiver

In the tunnel's Routes tab (older dashboards call it Public Hostname), select Add route > Published application:

  • Subdomain: hooks, domain: example.com
  • Service URL: http://localhost:8000

Save. The tunnel should show a Healthy status on the Tunnels page.

Step 4: Verify

curl -i https://hooks.example.com/webhook/your_webhook_id \
  -X POST -H "Content-Type: application/json" -d '{"test": true}'

Your receiver answers over valid HTTPS. For an authenticated webhook a 401 response is the expected proof that traffic reaches the receiver and authentication is enforced. Give your provider the URL https://hooks.example.com/webhook/your_webhook_id.

Option B: Proxied DNS (server already public)

If your receiver already sits on a public server with nginx and a certificate (see our nginx + Let's Encrypt guide), you can simply create the DNS record in Cloudflare with the proxy (orange cloud) enabled. You get Cloudflare's DDoS protection and IP hiding on top of your existing setup. Set SSL/TLS mode to Full (strict) in the Cloudflare dashboard so traffic between Cloudflare and your origin stays verified end to end.

Webhook-specific notes

  • Signatures survive proxying. HMAC signatures (GitHub's X-Hub-Signature-256, Stripe's Stripe-Signature) are computed over the raw request body, which Cloudflare passes through unmodified. Signature validation in your receiver keeps working unchanged.
  • Do not rely on client IP whitelisting through the proxy. Behind Cloudflare, the TCP source address your receiver sees belongs to Cloudflare. Prefer signature validation; it authenticates the sender cryptographically regardless of network path.
  • Free plan limits. Cloudflare's free proxy has a 100 MB request body cap, far above any webhook provider's payload limit (GitHub caps at 25 MB), so this is not a practical constraint.
  • Timeouts. Respond fast and process async. Core Webhook Module acknowledges the delivery and hands the payload to its destination module, so slow downstreams do not cause provider-side timeouts.

Next step: a full provider walkthrough, for example receiving GitHub webhooks or receiving Stripe webhooks.

Subscribe to Free Webhook Tool

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe