The tunnel works on every plan, including Free, and so do request capture and replay. One provider-specific tool is worth knowing first: the Stripe CLI can forward Stripe events to localhost without any tunnel, but it only handles Stripe.
What do Stripe, GitHub and Shopify require from a webhook URL?
All three post to a public HTTPS endpoint and expect a fast 2xx; they differ in timeouts, signatures and retries:
| Requirement | Stripe | GitHub | Shopify |
|---|---|---|---|
| Endpoint | Registered endpoints must be publicly accessible HTTPS URLs | HTTPS recommended; keep SSL verification enabled | HTTPS; Shopify verifies SSL certificates |
| Expected response | 2xx, returned quickly; redirects count as failures | 2XX within 10 seconds | 200 OK within 5 seconds (1-second connect timeout); redirects count as errors |
| Signature header | Stripe-Signature | X-Hub-Signature-256 (with a webhook secret) | X-Shopify-Hmac-SHA256 |
| Retries | Up to 3 days in live mode; 3 times over a few hours in a sandbox | Manual redelivery | 8 retries over 4 hours |
| Built-in local option | Stripe CLI stripe listen --forward-to | smee.io proxy in GitHub's testing guide | shopify app dev creates a tunnel whose URL changes each run |
Step 1: Run a webhook receiver on port 3000
This minimal Express app logs each delivery and returns 200. It keeps the raw body, which every signature check above needs.
// npm install express
const express = require("express");
const app = express();
app.post("/webhooks/:provider", express.raw({ type: "*/*" }), (req, res) => {
console.log(req.params.provider, {
stripe: req.get("stripe-signature"),
github: req.get("x-hub-signature-256"),
shopify: req.get("x-shopify-hmac-sha256"),
});
console.log(req.body.toString("utf8"));
// Verify the signature against the raw body before acting on the event
res.sendStatus(200);
});
app.listen(3000, () => console.log("http://localhost:3000"));
Step 2: Open a tunnel with zorven 3000
Install the client, save the token from the dashboard and start the tunnel:
zorven authtoken zrv_live_...
zorven 3000
The output shows an https://<name>--<slug>.zorven.app address with a valid certificate. For a short session you can open a tunnel that deletes itself:
zorven ephemeral 3000 --ttl 2h
Step 3: Register the URL with each provider
- Stripe: in Workbench > Webhooks, create an event destination with endpoint
https://<name>--<slug>.zorven.app/webhooks/stripeand copy thewhsec_signing secret. - GitHub: in the repository's Settings > Webhooks, set the payload URL to
.../webhooks/githuband add a secret. - Shopify: subscribe your app's topics to
.../webhooks/shopify. - Trigger a test event from the provider.
Step 4: Inspect and replay deliveries
Turn on Capture on the Logs page of the dashboard (it is off by default; an owner or admin enables it). From then on you see each request's headers and body. Select a delivery and replay it to send the same request to your local app again, instead of waiting for the provider's retry or clicking redeliver. "Edit and replay" lets you change the body, headers or path to test failure cases, and the replay diff compares the original and new responses. Captured bodies are capped at 256 KB by default.
On Pro and above, the policy engine's verify_webhook rule can also verify GitHub, Stripe, GitLab, Shopify and Slack signatures at the edge, rejecting invalid ones with 401 before they reach your machine.
Common pitfalls
- Does the URL change every time? Running
zorven 3000again from the same client on the same port keeps the same tunnel name, so you do not have to re-register.zorven ephemeraltunnels are deleted when their TTL ends. Custom domains are on Hobby and above; shortname.zorven.appnames on Pro and above. - The Free plan interstitial only appears on top-level browser HTML GET navigation; webhook POSTs are not affected.
- Body parsers break signatures. If a JSON middleware re-serialises the body, verification fails; read the raw body on webhook routes.
- CSRF middleware in Rails, Django or Laravel rejects webhook POSTs unless the route is exempt; the rejected request still shows up in the dashboard, which makes this easy to spot.
- Slow handlers time out (10 seconds on GitHub, 5 on Shopify). Return 2xx first and process later.
See Zorven for developers for demos, API testing and CI, pricing for plan limits, and the docs for every command. Related guides: CGNAT explained and Minecraft server without port forwarding; all guides are listed under guides.
When should you use something else?
- Stripe only: the Stripe CLI (
stripe listen --forward-to localhost:4242/webhook) needs no public URL and can also trigger test events withstripe trigger. Use a tunnel when you also receive webhooks from other providers or want one URL that behaves like production. - Shopify app development:
shopify app devalready sets up a tunnel; a separate tunnel helps when you want a URL that stays the same between runs. - Shared staging: if the whole team needs the events, point the provider at a staging server instead of one laptop.
Frequently asked questions
Sources
- Stripe Docs: Receive Stripe events in your webhook endpoint (accessed: 2026-11-08)
- GitHub Docs: Testing webhooks (accessed: 2026-11-08)
- GitHub Docs: Best practices for using webhooks (accessed: 2026-11-08)
- GitHub Docs: Validating webhook deliveries (accessed: 2026-11-08)
- Shopify Dev Docs: Deliver webhooks through HTTPS (accessed: 2026-11-08)
- Zorven docs: tunnels, request inspector and replay (accessed: 2026-11-08)
- Zorven pricing and plan limits (accessed: 2026-11-08)