<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Magic World</title>
    <description>The latest articles on DEV Community by Magic World (@yobox).</description>
    <link>https://dev.to/yobox</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981137%2F2f924e03-ddcd-497c-b085-cb7a2dd8dd03.png</url>
      <title>DEV Community: Magic World</title>
      <link>https://dev.to/yobox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yobox"/>
    <language>en</language>
    <item>
      <title>Webhook Testing: The Complete Guide for 2026</title>
      <dc:creator>Magic World</dc:creator>
      <pubDate>Fri, 12 Jun 2026 13:36:27 +0000</pubDate>
      <link>https://dev.to/yobox/webhook-testing-the-complete-guide-for-2026-22em</link>
      <guid>https://dev.to/yobox/webhook-testing-the-complete-guide-for-2026-22em</guid>
      <description>&lt;h1&gt;
  
  
  Webhooks are how modern systems talk to each other asynchronously. Stripe sends them. GitHub sends them. Shopify, Slack, Twilio, Postmark, SendGrid, Mailgun, Auth0, Clerk, Supabase, and every payment processor on the planet sends them. If your app integrates with anything, you're either receiving webhooks, sending them, or both.
&lt;/h1&gt;

&lt;h1&gt;
  
  
  And yet testing webhooks is still painful. The traditional setup involves ngrok tunnels, a local server you have to keep running, a way to replay payloads, and a way to debug the headers and signatures. This guide is the complete playbook for testing webhooks in 2026, including the patterns that finally let you do it from a browser tab.
&lt;/h1&gt;

&lt;h1&gt;
  
  
  What a Webhook Actually Is
&lt;/h1&gt;

&lt;p&gt;A webhook is an HTTP POST that a provider sends to a URL you give them when an event happens on their side. That's it. No magic, no special protocol, just HTTP.&lt;/p&gt;

&lt;p&gt;The challenge is everything around the POST:&lt;/p&gt;

&lt;h1&gt;
  
  
  Authentication. Most providers sign the payload with HMAC. You have to verify the signature before trusting the body.
&lt;/h1&gt;

&lt;p&gt;Delivery semantics. At-least-once delivery is the norm — you'll get duplicates. You need idempotency.&lt;br&gt;
Retries. Most providers retry on 5xx for hours. You need fast 2xx returns and a queue.&lt;br&gt;
Order. Webhooks are not ordered. charge.refunded can arrive before charge.succeeded.&lt;br&gt;
Latency. Some providers send within milliseconds; some take minutes.&lt;br&gt;
You need to test all of these, ideally without provisioning real infrastructure.&lt;/p&gt;
&lt;h1&gt;
  
  
  The Old Way: ngrok + a Local Server
&lt;/h1&gt;

&lt;p&gt;Classic flow: 1. ngrok http 3000 2. Copy the random URL. 3. Paste it into the provider's webhook config. 4. Trigger an event in the provider's UI. 5. Watch your terminal for the request. 6. Repeat for every payload variant.&lt;/p&gt;
&lt;h1&gt;
  
  
  Problems: - ngrok URLs rotate on the free tier; reconfigure provider every restart. - You need to actually write a server just to log the payload. - Comparing two payloads means scrolling terminal output. - Sharing a payload with a coworker means screenshots.
&lt;/h1&gt;
&lt;h1&gt;
  
  
  The New Way: Browser-Based Webhook Capture
&lt;/h1&gt;

&lt;p&gt;The YoBox Webhook Tester gives you a unique URL like &lt;a href="https://yobox.dev/webhook/" rel="noopener noreferrer"&gt;https://yobox.dev/webhook/&lt;/a&gt; that captures any HTTP request sent to it and shows you the headers, query, and body in real time, in the browser. No server. No ngrok. Works from CI.&lt;/p&gt;

&lt;p&gt;Use it when:&lt;/p&gt;
&lt;h1&gt;
  
  
  You're integrating with a new provider and want to see what they actually send.
&lt;/h1&gt;

&lt;p&gt;You're debugging why your endpoint isn't responding the way you expect.&lt;br&gt;
You're comparing payload shapes across event types.&lt;br&gt;
You need to share a payload with a coworker (just send the URL).&lt;br&gt;
You're running automated tests that need to assert on webhook delivery.&lt;br&gt;
A Concrete Testing Workflow&lt;br&gt;
Let's say you're integrating Stripe webhooks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a capture URL.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  Open YoBox Webhook Tester. You get &lt;a href="https://yobox.dev/webhook/" rel="noopener noreferrer"&gt;https://yobox.dev/webhook/&lt;/a&gt;
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Paste it into Stripe's dashboard.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  Subscribe to the events you care about (payment_intent.succeeded, charge.refunded, etc.).
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Trigger events in Stripe's test mode.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  Use Stripe's CLI: stripe trigger payment_intent.succeeded.
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Watch the request log fill in.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  You see the full headers (including Stripe-Signature), the query, and the JSON body. Inspect, copy, compare.
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Iterate on your real handler.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  Now you know exactly what Stripe sends. Build your handler against the real shape, not the docs (which are sometimes out of date or omit fields).
&lt;/h1&gt;
&lt;h1&gt;
  
  
  Testing Your Own Handler
&lt;/h1&gt;

&lt;p&gt;Once your handler is built, flip the flow. Have your test suite:&lt;/p&gt;
&lt;h1&gt;
  
  
  POST a known payload to your handler.
&lt;/h1&gt;

&lt;p&gt;Assert on the side effects (database row created, downstream API called, etc.).&lt;br&gt;
You can replay captured payloads from the Webhook Tester by copying the body and POSTing it back to your local server. For automated tests, use the YoBox Webhook API — your handler calls out to a downstream service in tests, you point that service at the YoBox capture URL, and your test asserts on what your handler sent.&lt;/p&gt;
&lt;h1&gt;
  
  
  Common Webhook Bugs to Test For
&lt;/h1&gt;

&lt;p&gt;These are the production bugs we see most often:&lt;/p&gt;
&lt;h1&gt;
  
  
  Signature verification skipped in dev
&lt;/h1&gt;

&lt;p&gt;Common pattern: if (process.env.NODE_ENV === 'development') skipVerify(). Then someone forgets to turn it back on. Always test that an unsigned request gets rejected.&lt;/p&gt;
&lt;h1&gt;
  
  
  Body parsed before signature verified
&lt;/h1&gt;

&lt;p&gt;Most signature schemes hash the raw body. If Express body-parser converts it to JSON first, the hash doesn't match. Use express.raw() for webhook routes specifically.&lt;/p&gt;
&lt;h1&gt;
  
  
  Idempotency missing
&lt;/h1&gt;

&lt;p&gt;The same payment_intent.succeeded event arrives twice. Your handler creates two database rows. Always include an idempotency check (typically by the provider's event ID).&lt;/p&gt;
&lt;h1&gt;
  
  
  Slow 2xx response
&lt;/h1&gt;

&lt;p&gt;Your handler takes 30 seconds to do downstream work. The provider times out at 10 and retries. Now you're processing the same event 6 times in parallel. Always return 2xx within 1 second and queue the actual work.&lt;/p&gt;
&lt;h1&gt;
  
  
  Out-of-order events
&lt;/h1&gt;

&lt;p&gt;charge.refunded arrives before charge.succeeded. Your handler crashes because the charge doesn't exist yet. Always fetch from the provider as source of truth; don't trust event order.&lt;/p&gt;
&lt;h1&gt;
  
  
  Test events leaking into production
&lt;/h1&gt;

&lt;p&gt;Stripe test-mode events have livemode: false. Always check it. We've seen production handlers process test events and refund real customers.&lt;/p&gt;
&lt;h1&gt;
  
  
  Webhook Testing in CI
&lt;/h1&gt;

&lt;p&gt;The full pattern:&lt;/p&gt;
&lt;h1&gt;
  
  
  Your CI spins up the app.
&lt;/h1&gt;

&lt;p&gt;A test generates a YoBox webhook URL.&lt;br&gt;
The test configures your app to send downstream webhooks to that URL.&lt;br&gt;
The test triggers the action.&lt;br&gt;
The test polls the YoBox API for the captured request.&lt;br&gt;
The test asserts on the headers and body.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```ts test('order completion fires webhook', async ({ request }) =&amp;gt; { const captureUrl = '&lt;a href="https://yobox.dev/webhook/" rel="noopener noreferrer"&gt;https://yobox.dev/webhook/&lt;/a&gt;' + crypto.randomUUID(); await configureMyApp({ webhookUrl: captureUrl }); await request.post('/orders', { data: { items: [...] } });&lt;/p&gt;

&lt;h1&gt;
  
  
  const captured = await pollWebhook(captureUrl); expect(captured.headers['x-myapp-event']).toBe('order.completed'); expect(captured.body.total).toBeGreaterThan(0); }); ```
&lt;/h1&gt;



&lt;h1&gt;
  
  
  Webhook Security Testing
&lt;/h1&gt;

&lt;p&gt;A security checklist for any webhook handler you write:&lt;/p&gt;

&lt;h1&gt;
  
  
  Signature is verified before any side effects. No DB write, no downstream call until hmac.equals() returns true.
&lt;/h1&gt;

&lt;p&gt;Timing-safe comparison. crypto.timingSafeEqual, not ===.&lt;br&gt;
Timestamp checked for replay. Most providers include a timestamp; reject if older than 5 minutes.&lt;br&gt;
Raw body preserved. Never reparse before verification.&lt;br&gt;
HTTPS only. Don't accept webhooks over HTTP.&lt;br&gt;
Allowlist sender IPs if the provider publishes them. Stripe, GitHub, and Twilio all publish IP ranges.&lt;br&gt;
Webhook Testing for Specific Providers&lt;br&gt;
Quick provider-specific notes:&lt;/p&gt;

&lt;h1&gt;
  
  
  Stripe: use the Stripe CLI's stripe listen + stripe trigger for local. Use YoBox Webhook Tester for staging.
&lt;/h1&gt;

&lt;p&gt;GitHub: the "Redeliver" button in the webhook UI is gold for testing handler changes.&lt;br&gt;
Shopify: test from the admin's webhook log, which can replay any past event.&lt;br&gt;
Twilio: the request inspector shows the full payload Twilio sent — pair with YoBox for archives.&lt;br&gt;
Slack: Slack signing secret has a specific concatenation format; test verification with a known-good payload.&lt;br&gt;
Auth0 / Clerk: both retry aggressively. Make sure your handler is idempotent before testing.&lt;br&gt;
See "Webhook Testing Without ngrok" for a deeper dive on each provider.&lt;/p&gt;

&lt;h1&gt;
  
  
  FAQ
&lt;/h1&gt;

&lt;p&gt;Do I still need ngrok for webhooks? Not for inspection. For actually receiving webhooks against code running on your laptop, yes. For just seeing what a provider sends, YoBox Webhook Tester replaces ngrok entirely.&lt;/p&gt;

&lt;p&gt;How do I test signed webhooks? Inspect with YoBox to confirm the signature format, then write a signature-generating helper for your tests that signs known payloads and POSTs them to your handler.&lt;/p&gt;

&lt;p&gt;How long do captured requests stay in YoBox? For the life of the token (typically up to several hours). Save what you need.&lt;/p&gt;

&lt;p&gt;Can I replay a captured webhook to my local server? Yes — copy the body and curl/POST it. The webhook log shows everything you need.&lt;/p&gt;

&lt;p&gt;Can webhook URLs be public? Anyone with the URL can POST to it. That's the point. Use rotation if you're worried about random traffic.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;a href="https://dev.tourl"&gt;&lt;/a&gt;Bottom Line
&lt;/h1&gt;

&lt;p&gt;Webhook testing in 2026 doesn't require ngrok, a local server, or a tunnel. Use the YoBox Webhook Tester for inspection and capture; build idempotency and signature verification into your handler; test the full async loop in CI by pairing webhook capture with disposable email for end-to-end coverage. Your future self — the one not debugging duplicate charges in production — will thank you.&lt;a href="https://dev.tourl"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>testing</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
