<?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: Adrian Orenes</title>
    <description>The latest articles on DEV Community by Adrian Orenes (@adrian_orenes_3d446ee5af8).</description>
    <link>https://dev.to/adrian_orenes_3d446ee5af8</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3579062%2F3846081a-c689-4c9f-bdb5-ce9b9200c0c0.jpg</url>
      <title>DEV Community: Adrian Orenes</title>
      <link>https://dev.to/adrian_orenes_3d446ee5af8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adrian_orenes_3d446ee5af8"/>
    <language>en</language>
    <item>
      <title>Debug a webhook locally: capture once, fix, and replay</title>
      <dc:creator>Adrian Orenes</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:51:55 +0000</pubDate>
      <link>https://dev.to/adrian_orenes_3d446ee5af8/debug-a-webhook-locally-capture-once-fix-and-replay-32pm</link>
      <guid>https://dev.to/adrian_orenes_3d446ee5af8/debug-a-webhook-locally-capture-once-fix-and-replay-32pm</guid>
      <description>&lt;p&gt;A webhook fails, you fix the handler, and now you need the same request again. Creating another checkout or pushing another commit gives you a new event. That makes it harder to tell whether your fix handles the original failure.&lt;/p&gt;

&lt;p&gt;This walkthrough uses &lt;a href="https://www.hooklistener.com" rel="noopener noreferrer"&gt;Hooklistener&lt;/a&gt;, which I work on, to forward requests to localhost and replay a captured delivery. Use a development or staging environment with test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Forward requests to your local handler
&lt;/h2&gt;

&lt;p&gt;Start your application on port 3000, then install the CLI and sign in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; hooklistener
hooklistener login
hooklistener tunnel &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tunnel prints a public HTTPS URL. If your handler is at &lt;code&gt;/webhooks/stripe&lt;/code&gt;, append that path to the URL when configuring your provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://YOUR-TUNNEL.hook.events/webhooks/stripe
    → http://localhost:3000/webhooks/stripe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the actual URL printed by the CLI. Send a test event from your provider, then check both your application logs and the captured request in Hooklistener. Requests passing through the tunnel are available for inspection and replay.&lt;/p&gt;

&lt;p&gt;If the provider already sends to a Hooklistener endpoint, you can forward that endpoint instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hooklistener listen &lt;span class="nt"&gt;--endpoint&lt;/span&gt; YOUR_ENDPOINT_ID &lt;span class="nt"&gt;--port&lt;/span&gt; 3000 &lt;span class="nt"&gt;--path-prefix&lt;/span&gt; /api/webhooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose the path prefix that matches your local routes. The &lt;a href="https://www.hooklistener.com/guides/cli-listen" rel="noopener noreferrer"&gt;listen reference&lt;/a&gt; explains the command options.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Inspect the delivery before changing code
&lt;/h2&gt;

&lt;p&gt;Open the captured request and check its method, path, headers, and raw body. Compare those with what your handler expects. A wrong route and a signature failure need different fixes, even if both look like a failed webhook in the provider dashboard.&lt;/p&gt;

&lt;p&gt;Keep the original event ID nearby. It helps you correlate the captured request, your logs, and any downstream job.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Replay the captured request
&lt;/h2&gt;

&lt;p&gt;After fixing the handler:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the original request in Hooklistener.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Replay&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the destination to your tunnel URL, including the handler path.&lt;/li&gt;
&lt;li&gt;Send the request and inspect the recorded response.&lt;/li&gt;
&lt;li&gt;Repeat against the same captured request after each change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sends a stored delivery to your chosen destination. It does not ask the provider to create a new event. Capture must already be in the delivery path: Hooklistener cannot reconstruct a request it never received.&lt;/p&gt;

&lt;p&gt;A successful HTTP response is only part of the check. Confirm that the expected development-side effect happened, such as a test order changing status or a job being queued.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two reasons a replay can still fail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Signature verification.&lt;/strong&gt; Some providers include a timestamp in the signed data. An older captured request can fail the receiver’s freshness check even when the payload is unchanged. Editing the body can also invalidate its signature. Keep signature verification enabled. Hooklistener supports re-signing Stripe, GitHub, and Slack replays through its MCP replay tool using a stored signing secret; stored secrets require a paid plan. Use the appropriate test-environment secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deduplication.&lt;/strong&gt; A replay retains the event ID. If your handler has already processed that ID, skipping it may be correct behavior. To test processing again, use isolated development state. Also test the duplicate path deliberately: processing one event twice should not create two orders or send two emails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the failure into a regression case
&lt;/h2&gt;

&lt;p&gt;Once the fix works, save the captured request as a replay case. Rerun it against development or staging after future changes, checking both the response and the side effect. Keep a duplicate-delivery case alongside it.&lt;/p&gt;

&lt;p&gt;The useful loop is small: capture a real test delivery, inspect it, change the handler, and replay the same input. You can debug the failure without repeatedly recreating the provider-side event.&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://www.hooklistener.com/cli-tunnel" rel="noopener noreferrer"&gt;CLI tunnel setup&lt;/a&gt; · &lt;a href="https://www.hooklistener.com/guides/replay-failed-webhooks" rel="noopener noreferrer"&gt;Replay guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>testing</category>
      <category>api</category>
    </item>
    <item>
      <title>I finally stopped using ngrok for Stripe Webhooks.</title>
      <dc:creator>Adrian Orenes</dc:creator>
      <pubDate>Fri, 27 Feb 2026 16:01:49 +0000</pubDate>
      <link>https://dev.to/adrian_orenes_3d446ee5af8/i-finally-stopped-using-ngrok-for-stripe-webhooks-nf7</link>
      <guid>https://dev.to/adrian_orenes_3d446ee5af8/i-finally-stopped-using-ngrok-for-stripe-webhooks-nf7</guid>
      <description>&lt;h1&gt;
  
  
  Debugging Stripe Webhooks without the Alt-Tab Fatigue
&lt;/h1&gt;

&lt;p&gt;I’ve spent an embarrassing amount of time in my career doing the "Stripe Dance." &lt;/p&gt;

&lt;p&gt;You know the one: Trigger a test event in the Stripe dashboard, alt-tab to the terminal to see if ngrok caught it, alt-tab to the editor to tweak a &lt;code&gt;console.log&lt;/code&gt;, then realize the ngrok tunnel timed out because you were over the free limit. &lt;/p&gt;

&lt;p&gt;The Stripe CLI helps a lot, but you’re still squinting at a terminal buffer trying to find that one &lt;code&gt;customer_id&lt;/code&gt; nested five levels deep in a JSON blob.&lt;/p&gt;

&lt;p&gt;I recently started using the &lt;strong&gt;Hooklistener MCP server&lt;/strong&gt; with Claude Code, and it’s the first time webhook integration hasn't felt like a chore. Here’s how the workflow actually feels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup (The boring part)
&lt;/h2&gt;

&lt;p&gt;If you're using Claude Code, Cursor, or Windsurf, you just drop this into your MCP config. No binaries to install, no daemon to manage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hooklistener"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"streamable-http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://app.hooklistener.com/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer hklst_your_key"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Workflow: Let the AI do the "Watching"
&lt;/h2&gt;

&lt;p&gt;The big shift here isn't just "seeing" the webhook; it's having the AI watch for it while you keep coding.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Creating a throwaway endpoint
&lt;/h3&gt;

&lt;p&gt;Instead of a permanent staging URL, I just ask Claude: &lt;em&gt;"Give me a new debug endpoint for Stripe Checkout."&lt;/em&gt; It hits the &lt;code&gt;create_endpoint&lt;/code&gt; tool and gives me a URL. I paste that into Stripe once. Unlike ngrok, this URL stays alive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjchiza41v4ojnhr36yde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjchiza41v4ojnhr36yde.png" alt=" " width="760" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The "Wait for it" moment
&lt;/h3&gt;

&lt;p&gt;This is where it clicked for me. I was working on an Elixir handler for &lt;code&gt;checkout.session.completed&lt;/code&gt;. I told Claude:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Wait for a request on the Stripe endpoint. I'm going to trigger a test event now."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I went to Stripe, hit "Send test webhook," and instead of me hunting for the log, &lt;strong&gt;the payload just appeared in my chat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwmnnqu3jl51yalrn1z1e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwmnnqu3jl51yalrn1z1e.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7xogoroh375artgptft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi7xogoroh375artgptft.png" alt=" " width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Writing the code with context
&lt;/h3&gt;

&lt;p&gt;Because the payload was right there in the conversation, I didn't have to copy-paste JSON into the prompt. I just said: &lt;em&gt;"Based on that payload, write the handler. Make sure it pulls the 'plan_id' from the metadata."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The AI isn't guessing based on outdated docs; it's looking at the &lt;strong&gt;exact&lt;/strong&gt; byte-for-byte request Stripe just sent. &lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually saves you
&lt;/h2&gt;

&lt;p&gt;I found two specific scenarios where this beats the old terminal-tailing method:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Comparing Events:&lt;/strong&gt; I captured a successful payment and a failed one. I asked Claude: &lt;em&gt;"Compare these two payloads. Why did the metadata missing in the second one?"&lt;/em&gt; It did a diff and realized I’d forgotten to pass the &lt;code&gt;client_reference_id&lt;/code&gt; in my frontend code. Doing that manually with two terminal windows is a headache.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Simulating the "What if":&lt;/strong&gt; Hooklistener lets you set up mock responses. I set the endpoint to return a &lt;code&gt;500&lt;/code&gt; to see how my app handled Stripe's retry logic. I could see the retry headers (&lt;code&gt;Stripe-Should-Retry&lt;/code&gt;) appearing in the &lt;code&gt;list_requests&lt;/code&gt; tool without leaving the editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is it perfect?
&lt;/h2&gt;

&lt;p&gt;No. You still have to go to the Stripe dashboard to paste the URL (Stripe doesn't have a "Create Webhook" API that works easily for local dev). And sometimes the AI gets over-excited and tries to explain the whole JSON structure when I just want one field.&lt;/p&gt;

&lt;p&gt;But compared to the "Alt-Tab Dance"? I'm never going back. If you’re already using an AI assistant, adding the Hooklistener MCP is a 2-minute upgrade that pays for itself the next time you have to debug a nested JSON object.&lt;/p&gt;

&lt;p&gt;Check it out at &lt;a href="https://hooklistener.com" rel="noopener noreferrer"&gt;hooklistener.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webhook</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
