<?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>Forem: James</title>
    <description>The latest articles on Forem by James (@jamesbrown).</description>
    <link>https://forem.com/jamesbrown</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%2F3845873%2F6855e5b9-71f7-406b-b71c-2711604633cb.png</url>
      <title>Forem: James</title>
      <link>https://forem.com/jamesbrown</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jamesbrown"/>
    <language>en</language>
    <item>
      <title>I compared the top 5 webhook debugging tools so you don't have to (here's what I found)</title>
      <dc:creator>James</dc:creator>
      <pubDate>Sun, 29 Mar 2026 02:21:43 +0000</pubDate>
      <link>https://forem.com/jamesbrown/i-compared-the-top-5-webhook-debugging-tools-so-you-dont-have-to-heres-what-i-found-4m5</link>
      <guid>https://forem.com/jamesbrown/i-compared-the-top-5-webhook-debugging-tools-so-you-dont-have-to-heres-what-i-found-4m5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://hooklog.c0c58bd.p.egbe.app/" rel="noopener noreferrer"&gt;Hooklog&lt;/a&gt; — webhook debugging for developers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webhooks</category>
      <category>debugging</category>
      <category>tools</category>
    </item>
    <item>
      <title>I spent a weekend building a webhook debugger and lost sleep over a silent Stripe failure</title>
      <dc:creator>James</dc:creator>
      <pubDate>Sat, 28 Mar 2026 10:07:31 +0000</pubDate>
      <link>https://forem.com/jamesbrown/i-spent-a-weekend-building-a-webhook-debugger-and-lost-sleep-over-a-silent-stripe-failure-4ogc</link>
      <guid>https://forem.com/jamesbrown/i-spent-a-weekend-building-a-webhook-debugger-and-lost-sleep-over-a-silent-stripe-failure-4ogc</guid>
      <description>&lt;h2&gt;
  
  
  Here is the thing nobody tells you about Stripe webhooks: they look simple until they are not.
&lt;/h2&gt;

&lt;p&gt;It was a Friday night. I had just shipped a new feature. Everything worked in development. In production? Silent failure. The payment succeeded, but my database never updated. No error. No warning. Just... nothing.&lt;/p&gt;

&lt;p&gt;Three hours of debugging later, I found the problem: my webhook handler was crashing on a malformed payload edge case, Stripe was retrying silently, and by the time I checked, the original event had long expired from the retry queue.&lt;/p&gt;

&lt;p&gt;Sound familiar? It turns out this is the most common webhook problem developers face.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I stopped that night and built &lt;a href="https://hooklog.c0c58bd.p.egbe.app/" rel="noopener noreferrer"&gt;Hooklog&lt;/a&gt; — a webhook proxy that gives you full visibility into what is actually hitting your endpoints.&lt;/p&gt;

&lt;p&gt;With Hooklog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live payload inspection&lt;/strong&gt; — see every webhook as it arrives, headers and body&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure alerting&lt;/strong&gt; — get emailed the moment a webhook returns 4xx or 5xx&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt; — re-send any captured webhook to debug your fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No signup required&lt;/strong&gt; — just create an endpoint and start sending webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It takes 30 seconds to get a URL and start debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;The existing tools (ngrok, Webhook.site, RequestBin) give you URL capture but miss the failure mode that costs developers the most: &lt;strong&gt;the silent failure&lt;/strong&gt;. The webhook that arrives, your server processes it wrong, returns 500, Stripe retries twice, gives up, and you never know it happened until a customer emails you three days later asking why their account was not updated.&lt;/p&gt;

&lt;p&gt;Hooklog is built specifically for this. It monitors your endpoint response codes and emails you on failure — so you know in minutes, not days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Built with Next.js, Express, and PostgreSQL. Dockerized, deployed to production over the weekend. Not a complex product — just a focused tool that solves one specific pain.&lt;/p&gt;

&lt;p&gt;If you have ever lost an evening to webhook debugging, you know exactly what I mean. &lt;a href="https://hooklog.c0c58bd.p.egbe.app/" rel="noopener noreferrer"&gt;Try Hooklog free&lt;/a&gt; — no signup required, 10k events/month on the free tier.&lt;/p&gt;

&lt;p&gt;Would love feedback from developers who have dealt with this firsthand.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webhooks</category>
      <category>stripe</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>The 3 Webhook Debugging Patterns That Would've Saved Me 3 Hours Last Week</title>
      <dc:creator>James</dc:creator>
      <pubDate>Sat, 28 Mar 2026 05:02:44 +0000</pubDate>
      <link>https://forem.com/jamesbrown/the-3-webhook-debugging-patterns-that-wouldve-saved-me-3-hours-last-week-1185</link>
      <guid>https://forem.com/jamesbrown/the-3-webhook-debugging-patterns-that-wouldve-saved-me-3-hours-last-week-1185</guid>
      <description>&lt;h1&gt;
  
  
  Dev.to Article 3: "The 3 Webhook Debugging Patterns That Would've Saved Me 3 Hours Last Week"
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>webhooks</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Test article from API</title>
      <dc:creator>James</dc:creator>
      <pubDate>Sat, 28 Mar 2026 01:08:08 +0000</pubDate>
      <link>https://forem.com/jamesbrown/test-article-from-api-4ek4</link>
      <guid>https://forem.com/jamesbrown/test-article-from-api-4ek4</guid>
      <description>&lt;p&gt;Test body&lt;/p&gt;

</description>
      <category>test</category>
    </item>
    <item>
      <title>The Webhook Failure Modes Nobody Warns You About</title>
      <dc:creator>James</dc:creator>
      <pubDate>Fri, 27 Mar 2026 21:09:39 +0000</pubDate>
      <link>https://forem.com/jamesbrown/the-webhook-failure-modes-nobody-warns-you-about-346m</link>
      <guid>https://forem.com/jamesbrown/the-webhook-failure-modes-nobody-warns-you-about-346m</guid>
      <description>&lt;h1&gt;
  
  
  The Webhook Failure Modes Nobody Warns You About
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Until you're staring at a 78-hour Stripe retry schedule wondering why your handler never fired.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Webhook integrations look simple. You point Stripe at your endpoint, you get a &lt;code&gt;200 OK&lt;/code&gt;, you're done.&lt;/p&gt;

&lt;p&gt;Then something breaks in production — and you have no idea what. Was it the payload? The signature? Your server? Stripe's retry queue? Something downstream?&lt;/p&gt;

&lt;p&gt;This isn't a guide to &lt;em&gt;building&lt;/em&gt; webhook endpoints. It's a field guide to the failures developers actually hit — and how to debug them faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Mode 1: The Silent Drop
&lt;/h2&gt;

&lt;p&gt;Your endpoint returns &lt;code&gt;200 OK&lt;/code&gt; to Stripe. But your handler never fires.&lt;/p&gt;

&lt;p&gt;What actually happened: your server accepted the request, but the payload was routed to a different part of your application that silently failed. Or the event was filtered by a middleware. Or the webhook was received but the database write failed and the error was swallowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The classic debug move:&lt;/strong&gt; pepper your handler with &lt;code&gt;console.log()&lt;/code&gt;. Deploy. Wait for Stripe to retry (up to 78 hours). Check logs. Repeat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Faster way:&lt;/strong&gt; capture the raw webhook before it reaches your server. See exactly what was sent, what your endpoint returned, and when — without touching your application code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Mode 2: The Empty Payload
&lt;/h2&gt;

&lt;p&gt;Stripe says it sent the event. Your handler receives it. But &lt;code&gt;event.data.object&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;{}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is almost always an API version mismatch — your endpoint is using a different Stripe API version than the one that generated the event. Stripe doesn't warn you; it just sends what it has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The catch:&lt;/strong&gt; you can't see the raw payload your endpoint received. You only see what your code parsed. And if the parsing failed silently, the debug path starts from the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; log the raw request body at the very top of your handler, before any parsing, before any middleware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Mode 3: The Signature Validation Loop
&lt;/h2&gt;

&lt;p&gt;You're validating the Stripe signature. It keeps failing. You regenerate your webhook secret. It still fails. You hardcode the raw payload to test — it works.&lt;/p&gt;

&lt;p&gt;The issue is almost always that you're validating the &lt;em&gt;parsed&lt;/em&gt; body instead of the &lt;em&gt;raw&lt;/em&gt; body. Stripe's signature is computed over the raw request body, not the JSON object your framework deserialized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong — app.use(bodyParser.json()) already parsed this&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// req.body is PARSED&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Right — use raw body&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// req.body is RAW&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever fought a Stripe signature validation error for an hour, you know this pain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Mode 4: The Infrastructure Block
&lt;/h2&gt;

&lt;p&gt;Your endpoint is behind an ngrok tunnel that expired. Your firewall is blocking incoming requests from Stripe's IP range. Your server restarted and the process isn't back up yet.&lt;/p&gt;

&lt;p&gt;Stripe reports delivery failures. Your server never received the event. Nobody knows why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; you have no visibility into whether your endpoint is actually reachable from the public internet. You find out something is wrong when Stripe emails you about failed deliveries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure Mode 5: The Retry Wait
&lt;/h2&gt;

&lt;p&gt;Your handler has a bug. You fix it. You need to test it.&lt;/p&gt;

&lt;p&gt;Stripe's retry schedule: &lt;strong&gt;1 hour, 12 hours, 72 hours.&lt;/strong&gt; That's potentially 78 hours of waiting to verify your fix works.&lt;/p&gt;

&lt;p&gt;For payment webhooks, Stripe will eventually retry — but waiting that long in development is brutal. The alternative is triggering test events manually through the Stripe CLI, which works but doesn't capture the full production scenario.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Better Debug Workflow
&lt;/h2&gt;

&lt;p&gt;The common thread in all these failures: &lt;strong&gt;you can't see what's actually happening&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A webhook debugging tool gives you a dedicated endpoint that sits between the sender (Stripe, GitHub, etc.) and your server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture&lt;/strong&gt; — the sender hits your debug endpoint instead of your server directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect&lt;/strong&gt; — you see the full raw payload, headers, and timing in a dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt; — you resend the exact payload to your server instantly, without waiting for retries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means you catch failures in seconds, not hours. You see what your server actually received — not what you hope it received.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hooklog — Webhook Debugging Without the Wait
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://hooklog.c0c58bd.p.egbe.app" rel="noopener noreferrer"&gt;Hooklog&lt;/a&gt; to solve exactly this. It's free (10k events/month), requires no signup, and gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A unique webhook URL you can point any service at immediately&lt;/li&gt;
&lt;li&gt;Full payload inspection: headers, body, response, timing&lt;/li&gt;
&lt;li&gt;One-click replay to test your handler without waiting for retries&lt;/li&gt;
&lt;li&gt;Email alerts when your endpoint returns 4xx or 5xx&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Get started:&lt;/strong&gt; &lt;a href="https://hooklog.c0c58bd.p.egbe.app" rel="noopener noreferrer"&gt;https://hooklog.c0c58bd.p.egbe.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free tier: 10,000 events/month, 3-day retention, up to 3 endpoints. No credit card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your worst webhook debugging story? Drop it in the comments — I read every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhook</category>
      <category>debugging</category>
      <category>stripe</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I Built a Webhook Debugger in a Weekend — Here's What I Learned</title>
      <dc:creator>James</dc:creator>
      <pubDate>Fri, 27 Mar 2026 10:21:43 +0000</pubDate>
      <link>https://forem.com/jamesbrown/i-built-a-webhook-debugger-in-a-weekend-heres-what-i-learned-2j1p</link>
      <guid>https://forem.com/jamesbrown/i-built-a-webhook-debugger-in-a-weekend-heres-what-i-learned-2j1p</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Webhook Debugger in a Weekend — Here's What I Learned
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Hooklog lets you capture, inspect, and replay any webhook with a single URL. No more blind integrations.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You're integrating Stripe webhooks into your Node.js app. You've read the docs, you think you've got it right — but nothing fires in production. Your local environment can't receive webhooks. You have no idea what's being sent, what's failing, or why.&lt;/p&gt;

&lt;p&gt;You add &lt;code&gt;console.log()&lt;/code&gt;. You deploy. You wait for Stripe to fire again (up to 78 hours, according to their retry schedule). Still nothing.&lt;/p&gt;

&lt;p&gt;This isn't a niche problem. It's every developer integrating Stripe, GitHub, Slack, Twilio, or any service that uses webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Solution
&lt;/h2&gt;

&lt;p&gt;Most developers solve this one of two ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ngrok + local server&lt;/strong&gt; — great until you hit rate limits, need to share with a teammate, or forget to restart ngrok after a laptop sleep&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write temporary logging middleware&lt;/strong&gt; — works once, then you delete it and lose the code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What nobody has is a &lt;strong&gt;Postman for webhooks&lt;/strong&gt;: one URL that captures everything, shows you what's broken, and lets you replay it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Hooklog
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://hooklog.c0c58bd.p.egbe.app/" rel="noopener noreferrer"&gt;Hooklog&lt;/a&gt; in a weekend. Here's the architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Core Idea
&lt;/h3&gt;

&lt;p&gt;Give every developer a unique webhook URL on sign-up (no sign-up required for the free tier):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://hooklog.c0c58bd.p.egbe.app/webhook/sec_your_endpoint_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point any webhook at it. Hooklog captures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request headers&lt;/li&gt;
&lt;li&gt;Request body (raw + parsed)&lt;/li&gt;
&lt;li&gt;Response from your endpoint&lt;/li&gt;
&lt;li&gt;Timing (time to first byte, total duration)&lt;/li&gt;
&lt;li&gt;HTTP status code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then you inspect it all in a dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14 (App Router) + Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Node.js + Express&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; JSONL files (MVP) — Postgres upgrade planned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Docker on internal PaaS gateway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe hosted checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Tricky Parts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Next.js 14 Dynamic Routes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next.js 14 changed how dynamic route &lt;code&gt;params&lt;/code&gt; work — they're now Promises that must be awaited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Next.js 13&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Next.js 14 — params is a Promise&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. File Storage + Directory Creation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpointId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endpointId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Don't forget recursive!&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.jsonl`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Docker Multi-Stage Build&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.next/ ./.next/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public/ ./public/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules/ ./node_modules/&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["npm", "start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Hooklog Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Capture Every Webhook&lt;/strong&gt; — point any webhook at your Hooklog URL and it captures everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Inspect in Real-Time&lt;/strong&gt; — headers, body, response status, timing, timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Replay Without Waiting&lt;/strong&gt; — click Replay and fire immediately instead of waiting for Stripe's retry schedule (1h, 12h, 72h).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Failure Alerts&lt;/strong&gt; — email immediately when your endpoint returns 4xx/5xx.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;Launched 2026-03-27. After 4 hours: 0 paying customers, 0 signups. Real Stripe test-mode webhooks are hitting it though.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Adding analytics to understand where users drop off&lt;/li&gt;
&lt;li&gt;Setting up proper authentication&lt;/li&gt;
&lt;li&gt;Migrating from JSONL to Postgres&lt;/li&gt;
&lt;li&gt;Google Ads to drive targeted traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try it: &lt;strong&gt;&lt;a href="https://hooklog.c0c58bd.p.egbe.app/" rel="noopener noreferrer"&gt;https://hooklog.c0c58bd.p.egbe.app/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Free tier: 10,000 events/month, 3-day retention, 3 endpoints.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have a webhook debugging horror story? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhook</category>
      <category>debugging</category>
      <category>devtools</category>
      <category>node</category>
    </item>
  </channel>
</rss>
