<?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: Praveen Doddamani</title>
    <description>The latest articles on DEV Community by Praveen Doddamani (@pkdoddamani).</description>
    <link>https://dev.to/pkdoddamani</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%2F4149654%2F47b828b5-9bed-4217-821d-7d1d983caccd.png</url>
      <title>DEV Community: Praveen Doddamani</title>
      <link>https://dev.to/pkdoddamani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pkdoddamani"/>
    <language>en</language>
    <item>
      <title>Why Stripe Webhook Signature Verification Fails on Replays (and How to Fix It)</title>
      <dc:creator>Praveen Doddamani</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:57:34 +0000</pubDate>
      <link>https://dev.to/pkdoddamani/why-stripe-webhook-signature-verification-fails-on-replays-and-how-to-fix-it-7f9</link>
      <guid>https://dev.to/pkdoddamani/why-stripe-webhook-signature-verification-fails-on-replays-and-how-to-fix-it-7f9</guid>
      <description>&lt;p&gt;You built a dead-letter queue. Your Next.js app, Express server, or AWS Lambda crashed on a Stripe &lt;code&gt;customer.subscription.updated&lt;/code&gt; event, your queue caught the failed payload, and you spent 30 minutes fixing a database migration bug.&lt;/p&gt;

&lt;p&gt;You deploy the fix, trigger a replay... and your application immediately throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Webhook signature verification failed: Timestamp outside the tolerance zone (1800s &amp;gt; 300s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every backend developer who attempts to build a resilient webhook ingestion pipeline runs headfirst into this wall. &lt;/p&gt;

&lt;p&gt;Here is why it happens, why common workarounds introduce severe vulnerabilities, and how to architect a zero-loss ingress buffer that downstream handlers can verify without code changes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;## 1. Anatomy of the 300-Second Signature Window&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Stripe dispatches a webhook, it includes the &lt;code&gt;Stripe-Signature&lt;/code&gt; header containing a Unix timestamp and an HMAC-SHA256 hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Stripe-Signature: t=1711234567,v1=5257a869e7eceeda32a1a243a0b7...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature &lt;code&gt;v1&lt;/code&gt; is computed across both the timestamp and the raw unparsed JSON payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;HMAC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhook_secret&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="nx"&gt;t&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="nx"&gt;rawBody&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call &lt;code&gt;stripe.webhooks.constructEvent(body, sig, secret)&lt;/code&gt; in your backend, the SDK performs two independent checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic Authenticity&lt;/strong&gt;: Does &lt;code&gt;v1&lt;/code&gt; match the HMAC of &lt;code&gt;${t}.${rawBody}&lt;/code&gt; using your secret?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay Protection&lt;/strong&gt;: Is &lt;code&gt;Math.abs(Date.now() / 1000 - t) &amp;lt;= 300&lt;/code&gt; (within 5 minutes)?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;### The Conflict&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replay attacks and legitimate Dead-Letter Queue (DLQ) replays look mathematically identical to Stripe's SDK. If your queue holds an event for longer than 300 seconds (5 minutes), standard signature verification will reject it every single time.&lt;/p&gt;




&lt;p&gt;*&lt;em&gt;## 2. Why Common Workarounds Fail&lt;br&gt;
*&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Bad Fix 1: Disabling verification on retried events
&lt;/h3&gt;

&lt;p&gt;Some teams add a header like &lt;code&gt;x-replayed: true&lt;/code&gt; and skip &lt;code&gt;constructEvent()&lt;/code&gt; if the header is present. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it fails:&lt;/strong&gt; Anyone who discovers your webhook URL can send a forged &lt;code&gt;customer.subscription.created&lt;/code&gt; payload with &lt;code&gt;x-replayed: true&lt;/code&gt; and grant themselves free access without ever hitting Stripe.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bad Fix 2: Bumping the SDK tolerance window
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dangerous: Opens a 3-day replay vulnerability&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;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="mi"&gt;86400&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Increasing tolerance to 72 hours allows retries to pass, but completely guts replay protection across your entire billing infrastructure, allowing stale or intercepted requests to be replayed.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;## 3. The Solution: Ingress Verify &amp;amp; Outbound Re-Signing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The correct architecture decouples &lt;strong&gt;Ingress Verification&lt;/strong&gt; from &lt;strong&gt;Internal Delivery&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Stripe] 
   │
   ▼ (Original Stripe Signature, t=now)
[Ingress Proxy] ── verifies signature within 300s window
   │
   ├─► Immediate 200 OK back to Stripe (stops retry timeouts)
   │
   ▼ (Persisted to durable SQLite / WAL storage)
[Outbound Dispatcher] 
   │
   ▼ (Fresh HMAC + Fresh Timestamp t=now + Provenance Headers)
[Downstream Handler (Your App)] ── constructsEvent() passes cleanly!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;### The Step-by-Step Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;At Ingress (&amp;lt;10ms)&lt;/strong&gt;: The proxy receives the webhook from Stripe. It uses your endpoint's signing secret to verify the original signature within the 300s tolerance window. It rejects fake requests upfront and acknowledges Stripe with an immediate &lt;code&gt;200 OK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable Storage&lt;/strong&gt;: The uncorrupted payload and metadata are written to persistent local storage (e.g. SQLite with WAL mode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Forward / Replay&lt;/strong&gt;: When delivering the event to your backend (whether 5ms later or 3 days later from a DLQ), the proxy computes a &lt;strong&gt;fresh timestamp&lt;/strong&gt; &lt;code&gt;t = Math.floor(Date.now() / 1000)&lt;/code&gt; and generates a valid &lt;code&gt;v1&lt;/code&gt; HMAC using your secret.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Re-Signing Function:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signStripeHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&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="nx"&gt;secret&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;freshTimestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&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;freshSignature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="nx"&gt;freshTimestamp&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="nx"&gt;rawBody&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&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="s2"&gt;`t=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;freshTimestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,v1=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;freshSignature&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;## 4. Protecting Against Out-of-Order Overwrites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One risk with replaying delayed webhooks is that a replayed &lt;code&gt;customer.subscription.deleted&lt;/code&gt; from 2 days ago might arrive &lt;em&gt;after&lt;/em&gt; a fresh &lt;code&gt;customer.subscription.created&lt;/code&gt; event from today.&lt;/p&gt;

&lt;p&gt;To prevent stale state from overwriting fresh state downstream, the ingress proxy should attach explicit provenance metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;x-hookarmor-is-replay: true
x-hookarmor-original-timestamp: 1711234567
x-hookarmor-delivery-id: del_8f92b1c
x-hookarmor-original-stripe-signature: t=1711234567,v1=...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your downstream app can check &lt;code&gt;x-hookarmor-original-timestamp&lt;/code&gt; against your database record's &lt;code&gt;updated_at&lt;/code&gt; before applying changes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;## 5. The Outcome: Zero Code Changes in Your App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the replayed request arrives with a fresh timestamp and a valid HMAC, your backend handler remains completely stock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your Next.js / Express route:&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;POST&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="nf"&gt;get&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="c1"&gt;// Passes on initial delivery AND on replay days later:&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&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;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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleBilling&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Open Source Solution: HookArmor
&lt;/h2&gt;

&lt;p&gt;I packaged this exact pattern into &lt;strong&gt;HookArmor&lt;/strong&gt;, an open-source (MIT), single-binary / Docker reverse proxy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instant &lt;code&gt;200 OK&lt;/code&gt; upstream + local SQLite (WAL) buffer.&lt;/li&gt;
&lt;li&gt;Automatic exponential retry backoff.&lt;/li&gt;
&lt;li&gt;Embedded Dead-Letter Queue (DLQ) with a web UI for 1-click manual replays.&lt;/li&gt;
&lt;li&gt;Automatic transparent Stripe re-signing with provenance headers.&lt;/li&gt;
&lt;li&gt;Zero external database dependencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can run it in front of your app with Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hookarmor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20-alpine&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sh -c "npm install -g hookarmor &amp;amp;&amp;amp; hookarmor start --port 8080 --target http://my-app:3000/webhooks"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HOOKARMOR_TARGET_URL=http://my-app:3000/webhooks&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HOOKARMOR_UI_PASSWORD=admin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/pkdoddamani/hookarmor" rel="noopener noreferrer"&gt;https://github.com/pkdoddamani/hookarmor&lt;/a&gt;&lt;br&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/hookarmor" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/hookarmor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How does your team currently handle delayed webhook retries and signature expiration? Would love to hear other architectural approaches in the comments!&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhook</category>
      <category>node</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why a strong API tool can still lose classic 'uptime' AI shortlists (Checkly practice audit)</title>
      <dc:creator>Praveen Doddamani</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:28:23 +0000</pubDate>
      <link>https://dev.to/pkdoddamani/why-a-strong-api-tool-can-still-lose-classic-uptime-ai-shortlists-checkly-practice-audit-1dji</link>
      <guid>https://dev.to/pkdoddamani/why-a-strong-api-tool-can-still-lose-classic-uptime-ai-shortlists-checkly-practice-audit-1dji</guid>
      <description>&lt;p&gt;I ran a &lt;strong&gt;practice&lt;/strong&gt; AI-answer visibility audit on a public developer tool (&lt;a href="https://www.checklyhq.com" rel="noopener noreferrer"&gt;Checkly&lt;/a&gt;) — not a customer engagement, and not a claim that CiteSprint "works for Checkly."&lt;/p&gt;

&lt;p&gt;The full tables (prompts, sources, direct site checks) are here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://citesprint.tech/sample-audit.html" rel="noopener noreferrer"&gt;Sample audit → citesprint.tech/sample-audit.html&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is the short version of what surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap isn't "robots.txt"
&lt;/h2&gt;

&lt;p&gt;Direct curls (Sep 19, 2026) on checklyhq.com found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;robots.txt&lt;/code&gt; allowing crawlers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/llms.txt&lt;/code&gt; live with a clear product definition&lt;/li&gt;
&lt;li&gt;homepage JSON-LD present&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FAQ &lt;code&gt;/faq&lt;/code&gt; → 404&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/vs/datadog&lt;/code&gt; → 404&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the site isn't "blocked from AI." The bigger hole is &lt;strong&gt;quotable buyer pages&lt;/strong&gt;: FAQs and honest "vs" pages that models (and humans) can retrieve when someone asks comparison questions.&lt;/p&gt;

&lt;p&gt;Files help clarity. They don't fix rank by themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same product, different wording → different shortlists
&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;API / synthetics / Playwright / monitoring-as-code&lt;/strong&gt; wording, Checkly often looks like a &lt;strong&gt;tie / partial&lt;/strong&gt; — shared shortlists with Datadog Synthetics, Postman Monitors, Better Stack, Hyperping, etc. (those cells are labeled &lt;em&gt;inferred&lt;/em&gt; from public 2026 roundups + Reditus citation maps — not live chat screenshots).&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;classic uptime&lt;/strong&gt; cluster ("best uptime monitoring tools"), a third-party &lt;strong&gt;measured&lt;/strong&gt; study tells a harsher story:&lt;/p&gt;

&lt;p&gt;Glotier, Aug 3, 2026 — ChatGPT + Gemini, 6 classic uptime prompts (full table in the sample audit):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Named in&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UptimeRobot&lt;/td&gt;
&lt;td&gt;6 / 6&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hyperping&lt;/td&gt;
&lt;td&gt;5 / 6&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Better Stack&lt;/td&gt;
&lt;td&gt;4 / 6&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkly&lt;/td&gt;
&lt;td&gt;not in named-12&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Checkly's positioning is stronger when buyers already speak "synthetics / as-code." Everyday "uptime" phrasing still tends to surface a different roster.&lt;/p&gt;

&lt;p&gt;That's the useful founder lesson: &lt;strong&gt;being named sometimes ≠ owning the highest-volume buyer wording.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd ship first (for any similar tool)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prompt baseline&lt;/strong&gt; — 15–25 buyer questions scored win / tie / lose, with &lt;em&gt;which assistants each report actually used&lt;/em&gt; (no invented multi-engine roster).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAQ + 2–3 honest vs pages&lt;/strong&gt; — markdown you can merge as a PR, written so assistants can quote you without inventing claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A few ethical mention targets&lt;/strong&gt; — awesome-lists, useful SO context, high-rep threads — draft the packet; the founder sends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-test the same prompts&lt;/strong&gt; — so movement is visible, not promised.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I productized that as a one-time sprint (&lt;a href="https://citesprint.tech" rel="noopener noreferrer"&gt;CiteSprint&lt;/a&gt;) with a founding ladder and a risk reducer: if the paid baseline already shows you're named on most agreed prompts, stop and refund.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honesty rules I won't break in public
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Practice demos cite &lt;strong&gt;direct site checks&lt;/strong&gt;, &lt;strong&gt;named third-party studies&lt;/strong&gt; (Glotier / Reditus), and clearly labeled &lt;strong&gt;inferred&lt;/strong&gt; roundups.&lt;/li&gt;
&lt;li&gt;I will not claim "we measured ChatGPT + Perplexity + Claude + AI Overviews on every prompt" unless the harness for that engagement actually did.&lt;/li&gt;
&lt;li&gt;No citation guarantees. Models change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the full tables and source list, start here: &lt;a href="https://citesprint.tech/sample-audit.html" rel="noopener noreferrer"&gt;sample Checkly audit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions / pushback welcome — especially if you've seen different naming on classic uptime vs synthetics wording in your own runs.&lt;/p&gt;

</description>
      <category>geo</category>
      <category>chatgpt</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
