Integrating third-party services via webhooks is a fundamental requirement for modern software architecture. Whether it is handling payment confirmations from Stripe, processing repository pushes via GitHub, or managing orders from Shopify, your application eventually needs to consume asynchronous events. In production, this is straightforward; your server resides at a public URL capable of receiving POST requests. However, local development on localhost:3000 creates a massive roadblock. Since your development environment is trapped behind your local network firewall, the global internet cannot reach it. Solving this issue requires specific tooling that bridges the gap between the public web and your private machine.
The Three Pillars of Webhook Testing
When evaluating how to bridge your development environment to the outside world, you generally encounter three distinct mechanisms. Understanding which one to use is the difference between a seamless debugging workflow and hours of fighting network configuration errors. The core categories are:
- Provider-specific CLIs: These are the most secure and reliable options, as they establish a direct outbound connection to the provider and deliver events directly to your local port without exposing a public URL.
- HTTPS Tunnels: These create a temporary public address that forwards incoming traffic to your local machine. These act as a universal fallback for any service that lacks its own CLI tool.
- Relay Services: These are sophisticated platforms that capture and store incoming event history. They are invaluable for teams, CI workflows, and situations where you need to replay events after the original session has terminated.
Provider CLIs: The Gold Standard
Whenever a service offers a native CLI tool, you should prioritize it over all other methods. Because these tools utilize a direct outbound connection, they eliminate the security risks associated with exposing your machine to the public internet. They also frequently provide built-in authentication and signature verification helpers.
For example, the Stripe CLI allows you to listen for events and forward them to your local endpoint with a single command. By running stripe listen --forward-to localhost:4242/webhook, you avoid the need for configuring public DNS or permanent webhook endpoints during development. Furthermore, the CLI allows you to trigger events on demand using stripe trigger payment_intent.succeeded, which is vital for testing edge cases.
Similarly, the GitHub CLI offers the gh webhook forward functionality. This feature effectively replaces older, less secure methods by forwarding events directly to a local URL. It is the cleanest way to build and test GitHub Apps or repository automation locally without relying on third-party proxy services.
The Universal Fallback: SSH Tunnels
When a provider lacks a custom CLI, you must rely on a tunnel. Pinggy is an industry-leading choice because it operates over the SSH protocol, which is natively supported on virtually every development environment. Unlike other tools that require heavy installations or configuration files, Pinggy is practically zero-config.
You can expose your local service with a command like:
ssh -p 443 -R0:localhost:3000 free.pinggy.io
This command generates a public HTTPS URL that you can immediately paste into your service's webhook dashboard. Because it uses SSH, it is incredibly lightweight and efficient. For debugging, Pinggy provides a built-in web-based inspector that allows you to view incoming headers, body content, and metadata in real-time. This is crucial for verifying that the payload structure matches what your backend expects.
Relay Services and CI Integration
If you require persistence, where webhook events must remain accessible even after your terminal session closes, you should reach for a relay service. Tools like the Hookdeck CLI or Svix Play allow you to capture events, store them in a dashboard, and replay them whenever necessary. This is especially helpful when working in a team environment where multiple developers need to inspect the same historical event payloads.
Furthermore, these services provide robust APIs. For instance, Svix Play allows your CI/CD pipelines to query the history of captured requests. This enables automated testing of webhooks within your deployment pipeline, where your test suite sends an event and subsequently verifies that the payload was received correctly, ensuring your integration logic is sound before it ever reaches production.
The Common Pitfall: Raw Body Verification
Regardless of which tool you select, the most frequent point of failure in webhook development is signature verification. Most modern web frameworks, such as Express, include middleware that automatically parses the request body as JSON. Unfortunately, this process often re-serializes the body in a way that modifies the whitespace or object order, causing the HMAC signature verification to fail. Because providers sign the exact bytes they send, any modification—even a minor formatting change—breaks the signature.
The solution is to configure your webhook endpoint to accept the raw request buffer. In Express, you achieve this by utilizing express.raw() specifically on the webhook route:
const express = require("express");
const crypto = require("crypto");
const app = express();
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("Stripe-Signature");
// Perform HMAC validation here using req.body as a buffer
});
By treating the incoming request as raw bytes, you ensure that your crypto verification matches the signature generated by the provider. Always remember to perform your signature checks before attempting to parse the payload as JSON, and never process the data before the verification has passed.
Designing for the Reality of Retries
Webhook delivery is inherently unreliable. Providers operate on an at-least-once delivery guarantee, meaning they will frequently send duplicate events to ensure receipt. Your handler must be idempotent. The most effective strategy is to store a unique event identifier—usually provided in the header or the JSON body—in your database.
When a request arrives, check your database or a cache layer to see if that specific ID has already been processed. If it exists, return a 200 OK status immediately without performing any downstream business logic. This prevents double-processing, which is critical for operations like processing payments or triggering system emails.
Additionally, you should always return a 2xx status code before starting any time-consuming processing. If you wait until your database operations are complete to respond, the provider might time out the request and mark it as a failure, triggering unnecessary retries and potential race conditions in your system.
Troubleshooting Strategies
When integration fails, do not assume your code is the culprit. Start by inspecting the headers and the exact payload. Tools like Pinggy allow you to replay requests exactly as they were received. If you are experiencing signature verification issues, use a dedicated HMAC tool to verify the signature offline against the payload bytes. This is often faster than debugging within the application layer.
Check for common configuration mismatches:
- Are you using the correct environment variables (e.g., test key vs. production key)?
- Is your signature verification using the correct HMAC algorithm?
- Does your server handle the timing-safe comparison correctly?
By systematically ruling out these variables, you can isolate issues quickly. Always ensure your error logging captures the full raw payload during failure states so you can replicate the exact conditions locally.
Conclusion
Choosing the right tool is the first step toward building resilient integrations. Provider-specific CLIs remain the best choice when available, followed by robust tunneling solutions like Pinggy for general-purpose testing. For enterprise-grade workflows requiring persistence and CI assertions, relay services like Hookdeck or Svix offer unmatched capabilities. Regardless of your choice, focusing on idempotency, secure signature verification, and raw payload handling will ensure your webhooks remain stable and reliable under any conditions.



Top comments (1)
Nice breakdown. The CLI > tunnel > relay order matches my experience. Two things I'd add:
1) With tunnels, most "signature verification failed" bugs come from the framework parsing the body before you verify it. Verify against the raw bytes (express.raw() on that route), not the re-serialized JSON.
2) For providers with no CLI, the cheapest loop is to capture one real delivery (raw body + headers), then replay it at localhost as many times as you need, including twice in a row to test idempotency. Disclosure: I built MockLane, which does capture/replay and runs it as a CI step. webhook.site is a free option for the capture half.
PITCH (60 words, if you'd rather contact the author): Hi, enjoyed your webhook dev guide, especially the relay/CI section. I built MockLane: capture any webhook, replay it at localhost or in CI, plus hosted mock APIs and an email sandbox. The free plan needs no card (1k captures, 100 CI scenario runs a month). Would it fit your relay section? Happy to give you a walkthrough.