Operational Experience
Testing and Local Development
Webhooks invert the usual development loop. Instead of your code calling an API, a provider calls your code — which means it needs a public HTTPS URL, real events, and a valid signature before your handler runs at all. Good providers recognize this and give developers ways to close that feedback loop.
Getting events to localhost
Tunnels expose a local port on a temporary public HTTPS URL, which you register as the webhook endpoint while developing. Stripe's own docs suggest ngrok for this; Hookdeck's CLI and Cloudflare Tunnel are common alternatives. Most tunnels also include a request inspector so you can see the exact headers and raw body the provider sent.
Provider CLIs skip the public URL entirely by forwarding events to localhost:
- Stripe CLI:
stripe listen --forward-to localhost:4242/webhookforwards events from your account and prints a signing secret for local verification;stripe trigger payment_intent.succeededcreates the objects that cause an event. See Stripe. - Shopify CLI:
shopify app webhook triggersends a sample payload for a topic to an address such ashttp://localhost:3000/webhooks. Shopify notes its payloads are always the same and failed deliveries aren't retried, so it doesn't replace end-to-end testing.
Triggering test events
- Ping and test events: GitHub sends a
pingevent when a webhook is created, and GitLab offers a "Test" button that also re-enables a disabled webhook on a2xxresponse. - Sandbox accounts: Stripe sandboxes generate real events with a separate signing secret — Stripe uses a different secret for test and live on the same endpoint.
- Redelivery: GitHub, GitLab, and Stripe let you resend a past delivery, which is the fastest way to reproduce a bug with a real payload. See Retries and Timeouts.
Testing what can go wrong
The happy path is the easy part. Before going live, make sure your listener:
- Rejects bad signatures. Send a request with a missing or altered signature header and confirm you return
401. Discord enforces this for you: its automated security checks send requests with invalid signatures to interaction endpoints and remove the URL if the endpoint fails validation. - Verifies against the raw body. Many frameworks parse JSON before your handler runs; re-serializing it changes the bytes and breaks HMAC verification. Stripe, Slack, Shopify, and Linear all call this out.
- Rejects stale timestamps. Replay a captured request after your tolerance window and confirm it fails — see Replay Prevention. This also means captured payloads stop verifying after a few minutes, so tests should sign fixtures with a test secret rather than replaying old requests.
- Handles duplicates and reordering. Deliver the same event twice and deliver events out of order. See Idempotency and Event Ordering.
- Responds before the timeout. Add artificial latency to processing and confirm the response still goes out first. See Retries and Timeouts.
Recommendations for providers
- Provide a CLI or dashboard action to send test events to any URL, including localhost
- Keep separate signing secrets for test and production environments
- Show delivery logs with request headers, payload, response status, and timing, and allow manual redelivery
- Publish sample payloads for every event type and complete, runnable verification code — see Documentation