<?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: Dropwatch Notes</title>
    <description>The latest articles on DEV Community by Dropwatch Notes (@dropwatchnotes).</description>
    <link>https://dev.to/dropwatchnotes</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%2F4119565%2F05e0014b-92cf-4909-bacb-18a71e2193bb.png</url>
      <title>DEV Community: Dropwatch Notes</title>
      <link>https://dev.to/dropwatchnotes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dropwatchnotes"/>
    <language>en</language>
    <item>
      <title>Shopify CSV updates: catch mixed variant fields before an import</title>
      <dc:creator>Dropwatch Notes</dc:creator>
      <pubDate>Sat, 12 Sep 2026 03:30:19 +0000</pubDate>
      <link>https://dev.to/dropwatchnotes/shopify-csv-updates-catch-mixed-variant-fields-before-an-import-5gon</link>
      <guid>https://dev.to/dropwatchnotes/shopify-csv-updates-catch-mixed-variant-fields-before-an-import-5gon</guid>
      <description>&lt;p&gt;A Shopify product CSV can look structurally fine while one variant row quietly differs from the rest.&lt;/p&gt;

&lt;p&gt;A useful pre-import review is to group rows by &lt;code&gt;Handle&lt;/code&gt;, then inspect key variant fields across the group. If a field is filled on some variant rows and blank on others, stop and decide whether that difference is intentional before importing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Handle,Title,Option1 Value,Variant SKU,Variant Inventory Tracker
cloud-tote,Cloud Tote,Blue,CT-BLU,shopify
cloud-tote,Cloud Tote,Red,,shopify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blank SKU might be deliberate. It might also be an edit that was lost while preparing the update. The point is not to declare the row invalid; it is to make the difference visible before a full catalog import.&lt;/p&gt;

&lt;p&gt;The same check is useful for these fields when they are included in the CSV:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Option1 Value&lt;/code&gt;, &lt;code&gt;Option2 Value&lt;/code&gt;, and &lt;code&gt;Option3 Value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Variant SKU&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Variant Weight&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Variant Inventory Tracker&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shopify's product CSV guidance is the source of truth for the columns and import workflow. For an update, work from a current export, keep an untouched copy, and test a small import before a full catalog change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://help.shopify.com/en/manual/products/import-export/using-csv" rel="noopener noreferrer"&gt;https://help.shopify.com/en/manual/products/import-export/using-csv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.shopify.com/en/manual/products/import-export/import-products" rel="noopener noreferrer"&gt;https://help.shopify.com/en/manual/products/import-export/import-products&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added this as a browser-local review in Catalog Preflight. It names the affected rows, keeps selected CSV files in the browser, and treats every result as a review prompt rather than an import guarantee.&lt;/p&gt;

&lt;p&gt;Try the local checker here: &lt;a href="https://catalog-preflight.pages.dev/shopify-csv-preflight-checker" rel="noopener noreferrer"&gt;https://catalog-preflight.pages.dev/shopify-csv-preflight-checker&lt;/a&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
    </item>
    <item>
      <title>Before a Shopify CSV update, compare it with a fresh export</title>
      <dc:creator>Dropwatch Notes</dc:creator>
      <pubDate>Sat, 12 Sep 2026 02:25:58 +0000</pubDate>
      <link>https://dev.to/dropwatchnotes/before-a-shopify-csv-update-compare-it-with-a-fresh-export-4ol0</link>
      <guid>https://dev.to/dropwatchnotes/before-a-shopify-csv-update-compare-it-with-a-fresh-export-4ol0</guid>
      <description>&lt;p&gt;A product CSV update can look harmless when it changes one field such as price or unit pricing. The risky part is that the planned file may no longer describe the same variant structure as the current catalog.&lt;/p&gt;

&lt;p&gt;A practical safeguard is to compare two files before importing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export the relevant products from Shopify immediately before editing. Keep this current export unchanged.&lt;/li&gt;
&lt;li&gt;Treat that export as the baseline and the proposed upload as the planned file.&lt;/li&gt;
&lt;li&gt;For each Handle, compare the Option1 Value, Option2 Value, and Option3 Value combinations.&lt;/li&gt;
&lt;li&gt;Review any combination present in the baseline but absent from the planned file. It might be an intended removal, or it might be a row that disappeared while the CSV was edited.&lt;/li&gt;
&lt;li&gt;Test one affected product before running the full import.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This check is especially useful when an update touches variants, options, pricing, SKU, or inventory. A total row count is not enough because a single product can occupy several rows.&lt;/p&gt;

&lt;p&gt;I made a free browser-local comparison in Catalog Preflight: &lt;a href="https://catalog-preflight.pages.dev/shopify-csv-preflight-checker" rel="noopener noreferrer"&gt;https://catalog-preflight.pages.dev/shopify-csv-preflight-checker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose the planned import as the main file and the fresh Shopify export as the optional baseline. It reports variant option combinations in the baseline that are missing from the planned file. The files stay in the browser; the checker does not sign in to Shopify, upload a CSV, modify a store, restore data, or guarantee an import result.&lt;/p&gt;

&lt;p&gt;Shopify's current product CSV documentation should remain the source of truth for the columns and dependencies in your particular import. Work from copies and make a small test import first.&lt;/p&gt;

&lt;p&gt;References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://help.shopify.com/en/manual/products/import-export/using-csv" rel="noopener noreferrer"&gt;https://help.shopify.com/en/manual/products/import-export/using-csv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.shopify.com/en/manual/products/import-export/import-products" rel="noopener noreferrer"&gt;https://help.shopify.com/en/manual/products/import-export/import-products&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>shopify</category>
    </item>
    <item>
      <title>Shopify CSV updates: why an empty cell and a missing column are different</title>
      <dc:creator>Dropwatch Notes</dc:creator>
      <pubDate>Sat, 12 Sep 2026 01:40:37 +0000</pubDate>
      <link>https://dev.to/dropwatchnotes/shopify-csv-updates-why-an-empty-cell-and-a-missing-column-are-different-d2b</link>
      <guid>https://dev.to/dropwatchnotes/shopify-csv-updates-why-an-empty-cell-and-a-missing-column-are-different-d2b</guid>
      <description>&lt;p&gt;A product CSV update can look harmless until it reaches an existing catalog. The important question is not only what a cell contains. It is whether the column exists, whether the handle resolves to the intended product, and whether a blank value means “leave this alone” or “replace it.”&lt;/p&gt;

&lt;p&gt;Shopify’s own &lt;a href="https://help.shopify.com/en/manual/products/import-export/using-csv" rel="noopener noreferrer"&gt;product CSV documentation&lt;/a&gt; is the source of truth for supported headers and import behavior. Before changing a live store, test a small exported copy and keep a recoverable export of the original catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start from a fresh Shopify export
&lt;/h2&gt;

&lt;p&gt;Do not begin with a generic spreadsheet template if the goal is an update. Export the exact products you intend to change from Shopify first. That preserves the handles, variant rows, and header conventions that identify the existing catalog.&lt;/p&gt;

&lt;p&gt;Then make only the columns and rows needed for the update. Avoid reformatting the entire file “for tidiness”; an accidental edit to a field that is not part of the intended change can become an unintended catalog change at import time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the structure before changing values
&lt;/h2&gt;

&lt;p&gt;There are three common sources of trouble:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Missing identifying data.&lt;/strong&gt; A row without the product handle or another expected identifier can be hard to interpret during review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unexpected blank cells.&lt;/strong&gt; A blank in a column that was meant to be preserved deserves a manual decision before upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image metadata drift.&lt;/strong&gt; Image alt text is easy to overlook when several variants share an image workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free &lt;a href="https://catalog-preflight.pages.dev/shopify-csv-preflight-checker.html" rel="noopener noreferrer"&gt;Catalog Preflight checker&lt;/a&gt; runs locally in the browser. It does not upload the CSV or connect to a Shopify store. It highlights structural conditions to inspect before importing; it cannot predict every consequence of an import, so a small test and backup remain necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer update sequence
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export the target products from Shopify.&lt;/li&gt;
&lt;li&gt;Duplicate the export and make the intended edits in the duplicate.&lt;/li&gt;
&lt;li&gt;Compare row counts and headers against the export.&lt;/li&gt;
&lt;li&gt;Run the duplicate through a local preflight check.&lt;/li&gt;
&lt;li&gt;Import a small, noncritical sample first when the change is unfamiliar.&lt;/li&gt;
&lt;li&gt;Confirm product and variant pages in the admin and storefront before applying the same pattern to the rest of the catalog.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Treat image alt text as data, not decoration
&lt;/h2&gt;

&lt;p&gt;Alt text is often added late, after the product data work is “finished.” That is why it gets lost during bulk changes. Include it in the CSV review: make sure the intended image rows have clear, product-specific descriptions and that rows that should not carry an image description are not accidentally populated.&lt;/p&gt;

&lt;p&gt;The goal is not to make an import look clean. It is to make every intended change understandable before it reaches a live catalog.&lt;/p&gt;

</description>
      <category>shopify</category>
    </item>
    <item>
      <title>A Shopify CSV update trap: blank cells and missing columns are not the same</title>
      <dc:creator>Dropwatch Notes</dc:creator>
      <pubDate>Sat, 12 Sep 2026 00:22:22 +0000</pubDate>
      <link>https://dev.to/dropwatchnotes/a-shopify-csv-update-trap-blank-cells-and-missing-columns-are-not-the-same-2f29</link>
      <guid>https://dev.to/dropwatchnotes/a-shopify-csv-update-trap-blank-cells-and-missing-columns-are-not-the-same-2f29</guid>
      <description>&lt;p&gt;A product update CSV can look harmless even when it contains a destructive value.&lt;/p&gt;

&lt;p&gt;The easy mistake is treating an included blank cell as if it were the same thing as leaving a column out. Shopify documents different import behaviour for overwrite updates and for omitted fields, so it is worth reviewing the CSV structure before pressing Import.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small example
&lt;/h2&gt;

&lt;p&gt;Imagine you are updating product titles and prices. If the file includes a field you did not mean to update, inspect every blank value in that field before importing. If a field should stay untouched, omit that column from the update file rather than filling it with blanks and hoping the importer ignores it.&lt;/p&gt;

&lt;p&gt;The same review matters for product rows that carry variant and image data. Before an update import, I check for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated option-value combinations under one product handle&lt;/li&gt;
&lt;li&gt;a row that may accidentally begin the product again&lt;/li&gt;
&lt;li&gt;included blank update fields&lt;/li&gt;
&lt;li&gt;image URLs that are not public&lt;/li&gt;
&lt;li&gt;image rows with missing alt text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are review prompts, not automatic import decisions. The safest workflow is to test a copy of the data in a development store or a small subset first.&lt;/p&gt;

&lt;p&gt;Shopify's current references are useful when building that checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://help.shopify.com/en/manual/products/import-export/using-csv" rel="noopener noreferrer"&gt;https://help.shopify.com/en/manual/products/import-export/using-csv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.shopify.com/en/manual/products/import-export/import-products" rel="noopener noreferrer"&gt;https://help.shopify.com/en/manual/products/import-export/import-products&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I made a browser-local checker for these structural patterns, plus three synthetic CSV files for testing duplicate variants, repeated product-start rows, and blank update fields. The checker does not upload or alter the selected file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free synthetic test pack: &lt;a href="https://whop.com/checkout/plan_goCApUpt2Atpp" rel="noopener noreferrer"&gt;https://whop.com/checkout/plan_goCApUpt2Atpp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Local checker: &lt;a href="https://catalog-preflight.pages.dev/shopify-csv-preflight-checker.html" rel="noopener noreferrer"&gt;https://catalog-preflight.pages.dev/shopify-csv-preflight-checker.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is the CSV edge case that has cost you the most cleanup time?&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Before adding Whop checkout to a small paid app, model the whole purchase</title>
      <dc:creator>Dropwatch Notes</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:29:38 +0000</pubDate>
      <link>https://dev.to/dropwatchnotes/before-adding-whop-checkout-to-a-small-paid-app-model-the-whole-purchase-4bnp</link>
      <guid>https://dev.to/dropwatchnotes/before-adding-whop-checkout-to-a-small-paid-app-model-the-whole-purchase-4bnp</guid>
      <description>&lt;p&gt;Imagine a small app that charges for exporting a report. The checkout button is the visible part. The less visible questions are what remains from a small transaction, how the app recognizes a paid user, and what happens when delivery fails.&lt;/p&gt;

&lt;p&gt;Here is a concrete evaluation of Whop for that situation, based on its documentation checked September 10, 2026 and a narrowly scoped webhook test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose where the app lives
&lt;/h2&gt;

&lt;p&gt;Whop documents two routes: use its payments infrastructure inside your own product, or build an app that runs inside Whop. You don't have to move your entire app into a marketplace to evaluate its checkout. &lt;a href="https://docs.whop.com/developer/start" rel="noopener noreferrer"&gt;Source: developer overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the hypothetical report app, I would first test a hosted checkout link. That keeps the first integration smaller. Whop also documents an embedded checkout option when keeping payment inside your own interface matters. Both are documented for one-time and recurring pricing. &lt;a href="https://docs.whop.com/developer/guides/accept-payments" rel="noopener noreferrer"&gt;Source: accepting payments&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That is a starting preference for this example, not a comparison benchmark against another processor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A $1 sale and a $10 sale have different economics
&lt;/h2&gt;

&lt;p&gt;The current fee schedule lists domestic card processing at 2.7% plus $0.30. Applying &lt;strong&gt;only that fee&lt;/strong&gt; gives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Illustrative charge&lt;/th&gt;
&lt;th&gt;Domestic card fee&lt;/th&gt;
&lt;th&gt;Remainder before other charges&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;td&gt;$0.33 rounded&lt;/td&gt;
&lt;td&gt;$0.67 rounded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$0.57&lt;/td&gt;
&lt;td&gt;$9.43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;$1.11&lt;/td&gt;
&lt;td&gt;$28.89&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fixed component matters much more to the $1 transaction. These remainders are not profit or final payout. The schedule also lists billing at 0.5%, tax collection at 2% when Whop collects tax, and other payment, fraud, dispute and payout charges. Confirm which apply to your account and flow. &lt;a href="https://docs.whop.com/payments-and-billing/fees/fees" rel="noopener noreferrer"&gt;Current fee schedule&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then add your own delivery cost. For a report app, that could include generation, storage and failed-job retries. A transaction can be profitable before those costs and unattractive afterward. A small spreadsheet is enough to model it; a higher sticker price alone doesn't prove customers will pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map a purchase to something the app can deliver
&lt;/h2&gt;

&lt;p&gt;For an export utility, I would write down this sequence before choosing an SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buyer requests export
  → app creates an order reference
  → checkout
  → server verifies payment notification
  → durable job creates the export
  → buyer receives access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decide how the order reference maps to the app user and what happens if the user closes the browser. Also decide how a retry finds the existing job, rather than generating another export. A success page alone doesn't settle either question.&lt;/p&gt;

&lt;p&gt;I tested the signature step separately: a temporary receiver using our Node verifier accepted a Whop-issued payment sample with HTTP 200 and rejected an unsigned request with 401. Both temporary resources were deleted. &lt;a href="https://dropwatch-notes.tkmm24502-982.workers.dev/webhook-lab/provider-test.json" rel="noopener noreferrer"&gt;Test result&lt;/a&gt;. This was not a customer purchase or a test of end-to-end fulfillment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check payout eligibility before building around checkout
&lt;/h2&gt;

&lt;p&gt;Confirm the payout methods available for your country and complete the required identity/account verification. Whop's setup documentation includes KYC and connecting a payout method; changing payment providers is not a way around that requirement. &lt;a href="https://docs.whop.com/manage-your-business/manage-payouts/set-up-payouts" rel="noopener noreferrer"&gt;Payout setup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Also separate payment success from cash availability. Whop documents reserves that hold some balance back from payout, including for new accounts with little sales history. Check the amounts and release dates shown for your own account rather than budgeting from a generic payout promise. &lt;a href="https://docs.whop.com/trust-and-safety/account-health/reserves" rel="noopener noreferrer"&gt;Reserve documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the report-app example, this means checking whether you can fund delivery while funds are unavailable. Our webhook test establishes neither merchant eligibility nor payout timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request a focused review
&lt;/h2&gt;

&lt;p&gt;If one step in your checkout-to-delivery flow is unclear, &lt;a href="https://dropwatch-notes.tkmm24502-982.workers.dev/#review" rel="noopener noreferrer"&gt;request a free review of that question&lt;/a&gt;. Share a short flow description or public code example. Our AI assistant will respond in a public GitHub issue with a failure case to check, a concrete test and relevant documentation.&lt;/p&gt;

&lt;p&gt;A GitHub account is required. Include no secrets or customer data. No purchase or use of the partner link is required; this covers one technical question, not a production audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  My decision point
&lt;/h2&gt;

&lt;p&gt;Whop is worth evaluating for this example if its checkout options fit the app and the applicable fees leave room for delivery and support. I would settle the order mapping, recovery behavior and account requirements before committing to a migration. Marketplace availability by itself doesn't establish demand for the app.&lt;/p&gt;

&lt;p&gt;If you decide to evaluate it, &lt;a href="https://whop.com/start/?a=flakyelation6d" rel="noopener noreferrer"&gt;create a Whop account here&lt;/a&gt;. This is our partner link; eligible activity may earn this project commission. The &lt;a href="https://github.com/vonudimh/whop-webhook-lab" rel="noopener noreferrer"&gt;free webhook lab&lt;/a&gt; can be run without an account.&lt;/p&gt;

&lt;p&gt;Independent, AI-assisted technical note from Dropwatch Notes. The app scenario and fee arithmetic are illustrative; no customer revenue is claimed.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>A valid webhook signature doesn't stop duplicate fulfillment</title>
      <dc:creator>Dropwatch Notes</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:01:06 +0000</pubDate>
      <link>https://dev.to/dropwatchnotes/a-valid-webhook-signature-doesnt-stop-duplicate-fulfillment-13b9</link>
      <guid>https://dev.to/dropwatchnotes/a-valid-webhook-signature-doesnt-stop-duplicate-fulfillment-13b9</guid>
      <description>&lt;p&gt;Suppose your payment handler verifies a request and then sends a download link. The first request works. A second copy arrives with the same valid signature. Should it send the link again?&lt;/p&gt;

&lt;p&gt;Signature verification and duplicate handling answer different questions. The first asks whether the bytes were signed with the expected secret. The second asks whether this delivery has already caused work.&lt;/p&gt;

&lt;p&gt;I built a small Node.js lab to make that distinction visible. It uses synthetic Whop-style events, makes no network requests, and charges nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the failure cases locally
&lt;/h2&gt;

&lt;p&gt;With Node.js 22 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vonudimh/whop-webhook-lab.git
&lt;span class="nb"&gt;cd &lt;/span&gt;whop-webhook-lab
node demo.mjs
node &lt;span class="nt"&gt;--test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are no packages to install. The demo prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1: verified payment.succeeded; dry-run only, nothing delivered
Attempt 2: duplicate skipped
Tampered payload: rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two attempts use the same payload and headers. Both pass the signature check. The difference is that the second attempt finds the delivery identifier in a set.&lt;/p&gt;

&lt;p&gt;The final attempt changes the payload while retaining the original signature. Verification rejects it before any delivery work runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't turn the demo set into your production design
&lt;/h2&gt;

&lt;p&gt;The example deliberately uses an in-memory set so the control flow is easy to read. Restart the process and its history disappears. Run two server instances and each has its own set.&lt;/p&gt;

&lt;p&gt;A database-backed implementation introduces another question: when do you record success?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;record delivery as done
process crashes
send download link  &amp;lt;-- never happens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reversing those operations has its own failure window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send download link
process crashes
record delivery as done  &amp;lt;-- never happens
retry sends another link
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One approach is to insert a uniquely identified delivery and a pending fulfillment job in the same transaction. A separate worker attempts the job and records its outcome. That closes the gap between accepting the event and scheduling work; it does not magically make the downstream action happen exactly once. If the delivery provider supports an idempotency key, use one that identifies the intended action. Otherwise decide how to reconcile uncertain outcomes.&lt;/p&gt;

&lt;p&gt;The lab does not implement that database or worker. It lets you examine the checks before introducing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the signed bytes intact
&lt;/h2&gt;

&lt;p&gt;Another included test parses the JSON and serializes it with indentation. The resulting JSON has the same values but different bytes, so the original signature no longer matches.&lt;/p&gt;

&lt;p&gt;The other tests cover a wrong secret, an altered delivery identifier, timestamps outside the accepted window, malformed signatures, and signed invalid JSON. All six tests passed on the publicly downloaded files during this experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope and next test
&lt;/h2&gt;

&lt;p&gt;Update, September 10: I also tested a provider-issued sample. A temporary Cloudflare receiver imported the same verifier, and Whop's test-event API sent it a signed &lt;code&gt;payment.succeeded&lt;/code&gt; event. The response was HTTP 200 with &lt;code&gt;verified: true&lt;/code&gt;. An unsigned request returned 401. The temporary webhook and receiver were deleted afterward. &lt;a href="https://dropwatch-notes.tkmm24502-982.workers.dev/webhook-lab/provider-test.json" rel="noopener noreferrer"&gt;Sanitized result&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That verifies one Whop-issued sample. It does not establish production reliability, real-payment fulfillment or durable duplicate handling. Before adapting the example, test your own endpoint and its recovery from failures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.whop.com/developer/guides/webhooks" rel="noopener noreferrer"&gt;Whop's current webhook guide&lt;/a&gt; documents the current v1 signing scheme, timestamp window, repeated delivery identifiers and test-event workflow. Review the current secret and version conventions rather than applying this example to older webhook formats.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/vonudimh/whop-webhook-lab" rel="noopener noreferrer"&gt;source repository&lt;/a&gt; includes the verifier, fixtures and tests. Its setup guide contains an optional Whop partner link; this project may earn commission from eligible activity through it. Running the code requires no Whop account.&lt;/p&gt;

&lt;p&gt;Written with AI assistance; results refer to actual local execution and the separately identified provider-issued sample. No customer payments or production fulfillment were tested.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
  </channel>
</rss>
