<?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: FetchSandbox</title>
    <description>The latest articles on DEV Community by FetchSandbox (@fetchsandbox).</description>
    <link>https://dev.to/fetchsandbox</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%2F3830177%2Fe63c5a3c-3f48-4ae2-ad6b-ed510712a396.png</url>
      <title>DEV Community: FetchSandbox</title>
      <link>https://dev.to/fetchsandbox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fetchsandbox"/>
    <language>en</language>
    <item>
      <title>How to test Stripe webhook signatures locally without breaking verification</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:24:17 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/how-to-test-stripe-webhook-signatures-locally-without-breaking-verification-1236</link>
      <guid>https://dev.to/fetchsandbox/how-to-test-stripe-webhook-signatures-locally-without-breaking-verification-1236</guid>
      <description>&lt;p&gt;&lt;em&gt;The request usually succeeds first. The signature check is where the time disappears.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most Stripe integrations do not fail on the first API call.&lt;/p&gt;

&lt;p&gt;You create the customer. You create the PaymentIntent. You confirm it. Everything looks fine. Then the webhook arrives and your handler says &lt;code&gt;invalid signature&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is usually the moment the debugging session starts.&lt;/p&gt;

&lt;p&gt;The annoying part is that the failure often has nothing to do with Stripe itself. It is usually your local setup. The payload got parsed too early. The raw body changed. The signature was generated against bytes your app never saw.&lt;/p&gt;

&lt;p&gt;One common example in Node/Express is using &lt;code&gt;express.json()&lt;/code&gt; on the webhook route. That middleware parses and re-serializes the body, which means the bytes you verify are no longer the bytes Stripe signed.&lt;/p&gt;

&lt;p&gt;Instead of this:&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;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="s2"&gt;/webhooks/stripe&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;json&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="s2"&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="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;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;webhookSecret&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="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;you usually need the raw body on that route:&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;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="s2"&gt;/webhooks/stripe&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="s2"&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="s2"&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="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;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;webhookSecret&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="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;The hard part is not just fixing the route once. It is trusting that the whole flow still works after the fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the webhook arrives&lt;/li&gt;
&lt;li&gt;the signature verifies&lt;/li&gt;
&lt;li&gt;your handler updates state correctly&lt;/li&gt;
&lt;li&gt;retries do not double-process the event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why webhook testing feels weirdly slippery. The API call and the webhook handler are two different systems, and most local setups only make one of them easy to observe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to test instead of the webhook in isolation
&lt;/h2&gt;

&lt;p&gt;What has helped me most is testing the full flow instead of testing the webhook in isolation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;trigger the upstream action&lt;/li&gt;
&lt;li&gt;inspect the exact webhook payload and headers&lt;/li&gt;
&lt;li&gt;verify the handler against the raw body&lt;/li&gt;
&lt;li&gt;confirm the final state change after the webhook is processed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step matters more than most examples admit. A verified signature is good. A verified signature plus the correct final state is what actually tells you the integration works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bug sticks around
&lt;/h2&gt;

&lt;p&gt;Webhook bugs are slippery because the API call and the webhook handler live on two separate timelines.&lt;/p&gt;

&lt;p&gt;The API request can succeed immediately. The event can arrive later. Your logs for one may be clean while the other is quietly failing. That is why developers end up jumping between local tunnels, dashboard events, request logs, and app state, trying to piece together what actually happened.&lt;/p&gt;

&lt;p&gt;The hard part is rarely "can I trigger a Stripe event?" The hard part is "can I trust the whole PaymentIntent plus webhook path enough to ship it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  A more useful local testing setup
&lt;/h2&gt;

&lt;p&gt;If you want a shortcut, use a &lt;a href="https://fetchsandbox.com/webhook-sandbox" rel="noopener noreferrer"&gt;webhook sandbox&lt;/a&gt; or a test environment that lets you inspect the full Stripe flow end-to-end instead of only replaying one event at a time.&lt;/p&gt;

&lt;p&gt;The useful part is not the mock payload. It is being able to see the request, the event, and the resulting state in one place.&lt;/p&gt;

&lt;p&gt;For Stripe-specific workflow context, this is also why a runnable &lt;a href="https://fetchsandbox.com/docs/stripe" rel="noopener noreferrer"&gt;Stripe portal&lt;/a&gt; is more useful than example requests alone.&lt;/p&gt;

&lt;p&gt;Curious what other people use here. Do you mostly replay events, tunnel to localhost, or test the full PaymentIntent plus webhook path every time?&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>How to test Stripe webhook signatures locally without breaking verification</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Wed, 15 Apr 2026 18:51:30 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/how-to-test-stripe-webhook-signatures-locally-without-breaking-verification-5ggk</link>
      <guid>https://dev.to/fetchsandbox/how-to-test-stripe-webhook-signatures-locally-without-breaking-verification-5ggk</guid>
      <description>&lt;p&gt;&lt;em&gt;The request usually succeeds first. The signature check is where the time disappears.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most Stripe integrations do not fail on the first API call.&lt;/p&gt;

&lt;p&gt;You create the customer. You create the PaymentIntent. You confirm it. Everything looks fine. Then the webhook arrives and your handler says &lt;code&gt;invalid signature&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is usually the moment the debugging session starts.&lt;/p&gt;

&lt;p&gt;The annoying part is that the failure often has nothing to do with Stripe itself. It is usually your local setup. The payload got parsed too early. The raw body changed. The signature was generated against bytes your app never saw.&lt;/p&gt;

&lt;p&gt;One common example in Node/Express is using &lt;code&gt;express.json()&lt;/code&gt; on the webhook route. That middleware parses and re-serializes the body, which means the bytes you verify are no longer the bytes Stripe signed.&lt;/p&gt;

&lt;p&gt;Instead of this:&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;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="s2"&gt;/webhooks/stripe&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;json&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="s2"&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="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;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;webhookSecret&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="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;you usually need the raw body on that route:&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;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="s2"&gt;/webhooks/stripe&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="s2"&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="s2"&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="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;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;webhookSecret&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="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;The hard part is not just fixing the route once. It is trusting that the whole flow still works after the fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the webhook arrives&lt;/li&gt;
&lt;li&gt;the signature verifies&lt;/li&gt;
&lt;li&gt;your handler updates state correctly&lt;/li&gt;
&lt;li&gt;retries do not double-process the event&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why webhook testing feels weirdly slippery. The API call and the webhook handler are two different systems, and most local setups only make one of them easy to observe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to test instead of the webhook in isolation
&lt;/h2&gt;

&lt;p&gt;What has helped me most is testing the full flow instead of testing the webhook in isolation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;trigger the upstream action&lt;/li&gt;
&lt;li&gt;inspect the exact webhook payload and headers&lt;/li&gt;
&lt;li&gt;verify the handler against the raw body&lt;/li&gt;
&lt;li&gt;confirm the final state change after the webhook is processed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step matters more than most examples admit. A verified signature is good. A verified signature plus the correct final state is what actually tells you the integration works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bug sticks around
&lt;/h2&gt;

&lt;p&gt;Webhook bugs are slippery because the API call and the webhook handler live on two separate timelines.&lt;/p&gt;

&lt;p&gt;The API request can succeed immediately. The event can arrive later. Your logs for one may be clean while the other is quietly failing. That is why developers end up jumping between local tunnels, dashboard events, request logs, and app state, trying to piece together what actually happened.&lt;/p&gt;

&lt;p&gt;The hard part is rarely "can I trigger a Stripe event?" The hard part is "can I trust the whole PaymentIntent plus webhook path enough to ship it?"&lt;/p&gt;

&lt;h2&gt;
  
  
  A more useful local testing setup
&lt;/h2&gt;

&lt;p&gt;If you want a shortcut, use a &lt;a href="https://fetchsandbox.com/webhook-sandbox" rel="noopener noreferrer"&gt;webhook sandbox&lt;/a&gt; or a test environment that lets you inspect the full Stripe flow end-to-end instead of only replaying one event at a time.&lt;/p&gt;

&lt;p&gt;The useful part is not the mock payload. It is being able to see the request, the event, and the resulting state in one place.&lt;/p&gt;

&lt;p&gt;For Stripe-specific workflow context, this is also why a runnable &lt;a href="https://fetchsandbox.com/docs/stripe" rel="noopener noreferrer"&gt;Stripe portal&lt;/a&gt; is more useful than example requests alone.&lt;/p&gt;

&lt;p&gt;Curious what other people use here. Do you mostly replay events, tunnel to localhost, or test the full PaymentIntent plus webhook path every time?&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>what actually happens when you provision a phone number in twilio</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Mon, 13 Apr 2026 06:06:55 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/what-actually-happens-when-you-provision-a-phone-number-in-twilio-11fd</link>
      <guid>https://dev.to/fetchsandbox/what-actually-happens-when-you-provision-a-phone-number-in-twilio-11fd</guid>
      <description>&lt;p&gt;i was trying to understand how twilio workflows actually behave end-to-end… not just from docs, but in practice&lt;/p&gt;

&lt;p&gt;so i ran a simple flow:&lt;br&gt;
provisioning a phone number&lt;/p&gt;

&lt;p&gt;instead of just calling the api and stopping there, i wanted to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what the request looks like
&lt;/li&gt;
&lt;li&gt;what the response returns
&lt;/li&gt;
&lt;li&gt;what webhook events get triggered
&lt;/li&gt;
&lt;li&gt;what the final state looks like
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  here’s the full flow
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/UCweHhp6ZkM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  what’s interesting
&lt;/h2&gt;

&lt;p&gt;a few things stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it’s not just request → response
&lt;/li&gt;
&lt;li&gt;webhook events are part of the flow
&lt;/li&gt;
&lt;li&gt;state changes matter just as much as the initial api call
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;most examples usually stop at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“here’s the request and response”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;but in real integrations, you need to care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when webhooks fire
&lt;/li&gt;
&lt;li&gt;how many times they fire
&lt;/li&gt;
&lt;li&gt;what state the resource ends up in
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  why this matters
&lt;/h2&gt;

&lt;p&gt;if you’re building integrations, especially anything async:&lt;/p&gt;

&lt;p&gt;just checking a 200 response isn’t enough&lt;/p&gt;

&lt;p&gt;you need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the system behaves over time
&lt;/li&gt;
&lt;li&gt;what events are emitted
&lt;/li&gt;
&lt;li&gt;how your system reacts to them
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  curious how others are testing this
&lt;/h2&gt;

&lt;p&gt;when you integrate with twilio (or similar apis):&lt;/p&gt;

&lt;p&gt;do you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;just rely on docs + sandbox?
&lt;/li&gt;
&lt;li&gt;simulate webhooks manually?
&lt;/li&gt;
&lt;li&gt;build your own test harness?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;would love to hear how others approach this&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>softwareengineering</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop mocking APIs. Use a stateful sandbox instead.</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Thu, 09 Apr 2026 12:32:56 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/stop-mocking-apis-use-a-stateful-sandbox-instead-3f6f</link>
      <guid>https://dev.to/fetchsandbox/stop-mocking-apis-use-a-stateful-sandbox-instead-3f6f</guid>
      <description>&lt;p&gt;Every developer has written this code:&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;// TODO: replace with real API call&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockUser&lt;/span&gt; &lt;span class="o"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;usr_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test@test.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then forgotten about the TODO. Then shipped it. Then found out in production that the real API returns &lt;code&gt;state&lt;/code&gt; not &lt;code&gt;status&lt;/code&gt;, and the ID format is &lt;code&gt;user_2xAbC&lt;/code&gt; not &lt;code&gt;usr_123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mocks lie. Sandboxes don't.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What if you could test against real API behavior — without production credentials?
&lt;/h2&gt;

&lt;p&gt;That's what &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; does. Give it any OpenAPI spec, and it provisions a stateful sandbox in seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /users&lt;/code&gt; creates a user that persists&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /users/{id}&lt;/code&gt; returns it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATCH /users/{id}&lt;/code&gt; with &lt;code&gt;{ "banned": true }&lt;/code&gt; updates the state&lt;/li&gt;
&lt;li&gt;Webhook events fire on every state change&lt;/li&gt;
&lt;li&gt;Invalid transitions return proper error codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No mock files. No Wiremock configs. No Postman collections to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  We just added 5 new APIs
&lt;/h2&gt;

&lt;p&gt;This week we onboarded five APIs developers actually struggle to test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Endpoints&lt;/th&gt;
&lt;th&gt;Why it's hard to test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fetchsandbox.com/docs/clerk" rel="noopener noreferrer"&gt;&lt;strong&gt;Clerk&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;td&gt;SSO flows need a real IdP, sessions expire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fetchsandbox.com/docs/resend" rel="noopener noreferrer"&gt;&lt;strong&gt;Resend&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;Actually sends emails, burns through quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fetchsandbox.com/docs/cohere" rel="noopener noreferrer"&gt;&lt;strong&gt;Cohere&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;API credits add up, RAG pipeline iteration is expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fetchsandbox.com/docs/svix" rel="noopener noreferrer"&gt;&lt;strong&gt;Svix&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;Webhook delivery fails to localhost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://fetchsandbox.com/docs/glean" rel="noopener noreferrer"&gt;&lt;strong&gt;Glean&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;Enterprise search needs connected data sources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each comes with curated workflows — real multi-step integration flows you can run instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works in 30 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create a sandbox&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://fetchsandbox.com/api/sandboxes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"spec_id":"clerk"}'&lt;/span&gt;

&lt;span class="c"&gt;# Returns: { "id": "a1b2c3", "credentials": [{ "api_key": "sandbox_..." }] }&lt;/span&gt;

&lt;span class="c"&gt;# 2. Use it like the real API&lt;/span&gt;
curl https://fetchsandbox.com/sandbox/a1b2c3/users &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sandbox_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email_address": ["dev@acme.com"], "first_name": "Jane"}'&lt;/span&gt;

&lt;span class="c"&gt;# 3. State persists — fetch the user you just created&lt;/span&gt;
curl https://fetchsandbox.com/sandbox/a1b2c3/users/&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sandbox_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Real stateful behavior, proper error codes, webhook events — all from the OpenAPI spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent-ready integration guide
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised us. We generate "integration guides" — Markdown files that contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every verified endpoint with exact request/response shapes&lt;/li&gt;
&lt;li&gt;State machine transitions (what can happen to a subscription after it's created?)&lt;/li&gt;
&lt;li&gt;Webhook events that fire at each step&lt;/li&gt;
&lt;li&gt;Sandbox credentials for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you hand this to a coding agent (Claude Code, Cursor, Copilot), the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the guide&lt;/li&gt;
&lt;li&gt;Inspects your existing repo patterns&lt;/li&gt;
&lt;li&gt;Asks about scope before implementing&lt;/li&gt;
&lt;li&gt;Builds working integration code using only verified data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;No hallucinated endpoints. No invented status codes. Every fact comes from sandbox execution.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We tested this with Paddle's subscription lifecycle. The agent built a complete billing dashboard — product catalog, checkout, subscription management with activate/pause/resume/cancel — from just the integration guide.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rnagulapalle/sandbox/tree/main/showcase/paddle-billing" rel="noopener noreferrer"&gt;See the code →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  49 APIs, 9,300+ endpoints
&lt;/h2&gt;

&lt;p&gt;The full catalog includes Stripe, Paddle, PayPal, Twilio, GitHub, OpenAI, Shopify, DigitalOcean, Datadog, and 40 more.&lt;/p&gt;

&lt;p&gt;Every spec gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic docs portal&lt;/li&gt;
&lt;li&gt;Workflow discovery&lt;/li&gt;
&lt;li&gt;Stateful sandbox&lt;/li&gt;
&lt;li&gt;Integration guides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;Browse the catalog →&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;We're onboarding new APIs every week. If there's an API you're struggling to test, &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;let us know&lt;/a&gt; — onboarding takes minutes if you have an OpenAPI spec.&lt;/p&gt;

&lt;p&gt;Next up: Deepgram, AssemblyAI, Stytch, Replicate, and Lago.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FetchSandbox is free to try. No signup required. Sandboxes run in your browser.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>got a 200 response in minutes… still took hours to know if anything worked</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Sun, 05 Apr 2026 00:05:58 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/got-a-200-response-in-minutes-still-took-hours-to-know-if-anything-worked-4j1b</link>
      <guid>https://dev.to/fetchsandbox/got-a-200-response-in-minutes-still-took-hours-to-know-if-anything-worked-4j1b</guid>
      <description>&lt;p&gt;i was integrating with Paddle recently&lt;/p&gt;

&lt;p&gt;got a 200 response pretty quickly&lt;br&gt;&lt;br&gt;
so i thought… ok this should be straightforward&lt;/p&gt;

&lt;p&gt;it wasn’t&lt;/p&gt;

&lt;p&gt;not because the API was hard&lt;br&gt;&lt;br&gt;
but because i couldn’t tell if anything actually worked&lt;/p&gt;

&lt;h2&gt;
  
  
  what’s easy now
&lt;/h2&gt;

&lt;p&gt;most APIs today are pretty good at getting you started&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;docs are cleaner
&lt;/li&gt;
&lt;li&gt;SDKs are better
&lt;/li&gt;
&lt;li&gt;you can make your first API call in minutes
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;that part is honestly not the problem anymore&lt;/p&gt;

&lt;h2&gt;
  
  
  where things slow down
&lt;/h2&gt;

&lt;p&gt;the time goes after that first 200&lt;/p&gt;

&lt;p&gt;you start asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did the webhook fire?
&lt;/li&gt;
&lt;li&gt;did it fire with the right payload?
&lt;/li&gt;
&lt;li&gt;did the state actually change?
&lt;/li&gt;
&lt;li&gt;is it the correct state or just “some state”?
&lt;/li&gt;
&lt;li&gt;what happens if this fails?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and there’s no single place to see this&lt;/p&gt;

&lt;p&gt;so you jump between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your terminal
&lt;/li&gt;
&lt;li&gt;webhook logs
&lt;/li&gt;
&lt;li&gt;dashboards
&lt;/li&gt;
&lt;li&gt;your own code
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;trying to figure out what actually happened&lt;/p&gt;

&lt;h2&gt;
  
  
  the loop
&lt;/h2&gt;

&lt;p&gt;this is usually how it goes:&lt;/p&gt;

&lt;p&gt;call API → looks fine&lt;br&gt;&lt;br&gt;
wait for webhook → nothing&lt;br&gt;&lt;br&gt;
retry → webhook shows up twice&lt;br&gt;&lt;br&gt;
check state → not what you expected&lt;br&gt;&lt;br&gt;
add logs → try again  &lt;/p&gt;

&lt;p&gt;repeat&lt;/p&gt;

&lt;p&gt;after a few tries you’re not even sure what you’re debugging anymore&lt;/p&gt;

&lt;h2&gt;
  
  
  what we optimize for
&lt;/h2&gt;

&lt;p&gt;most DX today is focused on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;how fast can you make your first API call&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;which is useful… but not enough&lt;/p&gt;

&lt;p&gt;because calling the API is not the hard part anymore&lt;/p&gt;

&lt;h2&gt;
  
  
  what actually matters
&lt;/h2&gt;

&lt;p&gt;what matters is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;how long it takes before you trust your integration&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;not “it returned 200”&lt;/p&gt;

&lt;p&gt;but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the system behaves the way you expect
&lt;/li&gt;
&lt;li&gt;state moves correctly
&lt;/li&gt;
&lt;li&gt;webhooks fire correctly
&lt;/li&gt;
&lt;li&gt;retries don’t break things
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;that’s what actually decides if you can ship&lt;/p&gt;

&lt;h2&gt;
  
  
  docs vs reality
&lt;/h2&gt;

&lt;p&gt;docs show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request
&lt;/li&gt;
&lt;li&gt;response
&lt;/li&gt;
&lt;li&gt;“success”
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;real life looks more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request succeeds
&lt;/li&gt;
&lt;li&gt;webhook missing
&lt;/li&gt;
&lt;li&gt;state is off
&lt;/li&gt;
&lt;li&gt;retry changes something else
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and now you’re debugging across multiple places&lt;/p&gt;

&lt;h2&gt;
  
  
  what we started doing
&lt;/h2&gt;

&lt;p&gt;after hitting this a few times, we stopped looking at just API calls&lt;/p&gt;

&lt;p&gt;and started looking at the whole flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create → update → confirm
&lt;/li&gt;
&lt;li&gt;state transitions
&lt;/li&gt;
&lt;li&gt;webhook events
&lt;/li&gt;
&lt;li&gt;final result
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;in one place&lt;/p&gt;

&lt;p&gt;not just:&lt;/p&gt;

&lt;p&gt;“did it return 200”&lt;/p&gt;

&lt;p&gt;but:&lt;/p&gt;

&lt;p&gt;“did it actually work”&lt;/p&gt;

&lt;p&gt;ended up building a small internal tool to run these flows end-to-end and see the final state instead of stitching logs manually&lt;/p&gt;

&lt;p&gt;turned it into something usable here: &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;https://fetchsandbox.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  still feels like a gap
&lt;/h2&gt;

&lt;p&gt;curious how others deal with this&lt;/p&gt;

&lt;p&gt;are teams actually measuring this properly?&lt;/p&gt;

&lt;p&gt;or are we all still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reading logs
&lt;/li&gt;
&lt;li&gt;retrying requests
&lt;/li&gt;
&lt;li&gt;and hoping it works in prod&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>devrel</category>
      <category>webdev</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Our partners kept breaking on staging. So we gave them production access. (Don't do this.)</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Wed, 01 Apr 2026 23:00:29 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/our-partners-kept-breaking-on-staging-so-we-gave-them-production-access-dont-do-this-1nna</link>
      <guid>https://dev.to/fetchsandbox/our-partners-kept-breaking-on-staging-so-we-gave-them-production-access-dont-do-this-1nna</guid>
      <description>&lt;p&gt;I work on an embed platform. Partners integrate our APIs to build features into their own products — payments, identity verification, onboarding flows.&lt;/p&gt;

&lt;p&gt;We spent six months trying to answer one question: &lt;strong&gt;how do partners test their integration before going live?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We tried four approaches. Each one failed in a new and exciting way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: "Just use our docs"
&lt;/h2&gt;

&lt;p&gt;We pointed partners to our API docs. Endpoints, auth, request/response examples. "You're good to go."&lt;/p&gt;

&lt;p&gt;Partners would start building, hit something we didn't document, open a support ticket, wait two days, lose momentum. A few never came back. One told us they went with a competitor because "we couldn't get a working test setup in a week."&lt;/p&gt;

&lt;p&gt;The docs described what &lt;em&gt;should&lt;/em&gt; happen. Partners had no way to see what &lt;em&gt;actually&lt;/em&gt; happens until they wrote code, deployed it, and hoped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: Internal UAT box
&lt;/h2&gt;

&lt;p&gt;Next idea: give partners access to our internal UAT environment. IP whitelisted, shared credentials. We told them "please don't break anything" without a hint of irony.&lt;/p&gt;

&lt;p&gt;Worked for three weeks.&lt;/p&gt;

&lt;p&gt;Then QA pushed a bad config on a Tuesday afternoon. Partner's integration broke. They didn't know it was our side — spent two days debugging their own code before opening a ticket. By the time we figured it out, their engineering lead was cc'ing our VP.&lt;/p&gt;

&lt;p&gt;Around the same time, another team was running load tests on UAT. Partner's API calls were timing out. Their CTO emailed us asking if our platform was "production-ready."&lt;/p&gt;

&lt;p&gt;UAT was built for us to break things. We invited external partners into that mess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: Staging with IP whitelisting
&lt;/h2&gt;

&lt;p&gt;We carved out a "partner staging" — same codebase, separate deployment, IP whitelisted per partner. This felt like the grown-up solution.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP whitelists were a full-time job.&lt;/strong&gt; Every new partner meant new firewall rules. Partners working from home had different IPs than their office. One partner's CTO was traveling and couldn't hit the sandbox from his hotel. We were debugging VPN configs at 11pm on a Thursday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test data went stale.&lt;/strong&gt; Partners tried to create orders for products that existed in production but not in staging. "Your API returns 404 for product_id XYZ." Yes, because nobody seeded staging in three weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploys kept colliding.&lt;/strong&gt; Engineers would ship to staging without checking if partners were actively testing. A deploy during a partner's live demo call is the kind of thing that gets brought up in quarterly business reviews.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Running a full mirror of production for partner testing. Database, compute, monitoring, on-call rotation. All for an environment that got used maybe 10 hours a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 4: Production access
&lt;/h2&gt;

&lt;p&gt;I wish I was kidding.&lt;/p&gt;

&lt;p&gt;The reasoning: "staging is flaky, UAT is a disaster, let's just whitelist them on production with read-only test accounts."&lt;/p&gt;

&lt;p&gt;The API was stable! Partners were happy. For about a month.&lt;/p&gt;

&lt;p&gt;Then one partner's integration bug created 400 orphaned records in production. Our data team spent a weekend cleaning it up.&lt;/p&gt;

&lt;p&gt;Compliance wanted to know why test payloads with fake PII were hitting the production database.&lt;/p&gt;

&lt;p&gt;When we needed to do maintenance, we had to coordinate downtime with partners who were "just running a few test calls."&lt;/p&gt;

&lt;p&gt;We'd built the world's most expensive and dangerous sandbox: production with IP whitelisting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually needed
&lt;/h2&gt;

&lt;p&gt;After burning six months on this, the answer felt obvious in hindsight. Partners didn't need access to &lt;em&gt;our&lt;/em&gt; infrastructure at all.&lt;/p&gt;

&lt;p&gt;They needed an API that looked and acted like ours — same endpoints, same schemas, same auth patterns — but was completely separate. Something where POST actually creates a resource, GET retrieves it, state transitions work, and nobody else's deploy can break it.&lt;/p&gt;

&lt;p&gt;Not a mock server (those are stateless — step 2 doesn't know about step 1). Not a shared staging box (those are everybody's problem). A dedicated, isolated sandbox generated from the same OpenAPI spec our real API uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building now
&lt;/h2&gt;

&lt;p&gt;This experience is half the reason I started working on &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt;. You give it an OpenAPI spec and it spins up a stateful sandbox — real CRUD, state machines, webhook events, seed data. No infrastructure to manage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Partner gets a sandbox from your spec&lt;/span&gt;
npx fetchsandbox generate ./your-api-openapi.yaml

&lt;span class="c"&gt;# They can call endpoints immediately&lt;/span&gt;
curl https://your-api.fetchsandbox.com/v1/orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"api-key: sandbox_abc123"&lt;/span&gt;
&lt;span class="c"&gt;# → 200 OK, returns realistic seed data&lt;/span&gt;

&lt;span class="c"&gt;# They can run the full integration workflow&lt;/span&gt;
fetchsandbox run your-api create-and-fulfill-order
&lt;span class="c"&gt;# → ✓ Create order — 201&lt;/span&gt;
&lt;span class="c"&gt;# → ✓ Add line items — 200&lt;/span&gt;
&lt;span class="c"&gt;# → ✓ Submit for fulfillment — 200&lt;/span&gt;
&lt;span class="c"&gt;# → ✓ Webhook: order.fulfilled fired&lt;/span&gt;
&lt;span class="c"&gt;# → All steps passed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No staging environment to maintain. No IP whitelists. No risk to production. Partners get their own URL, their own data, their own credentials. Your team doesn't touch it.&lt;/p&gt;

&lt;p&gt;When a partner asks "does this workflow work?" — they prove it themselves. No support ticket needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The time sink nobody tracks
&lt;/h2&gt;

&lt;p&gt;If you run a partner API, add up how many hours your team spends per month on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisioning and maintaining test environments&lt;/li&gt;
&lt;li&gt;Debugging "is your sandbox down?" tickets&lt;/li&gt;
&lt;li&gt;Managing IP whitelists and VPN access&lt;/li&gt;
&lt;li&gt;Re-seeding stale test data&lt;/li&gt;
&lt;li&gt;Coordinating deploys around partner testing schedules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At my company it was easily 20-30 hours a month across engineering and DevOps. For a problem that shouldn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; works with any OpenAPI 3.x spec. 19 APIs are live — Stripe, GitHub, Twilio, WorkOS, and more. No signup.&lt;/p&gt;

&lt;p&gt;If you run an API platform and want to try this with your own spec, reach out — &lt;a href="https://x.com/fetchsandbox" rel="noopener noreferrer"&gt;@fetchsandbox&lt;/a&gt; on X or &lt;a href="mailto:hello@fetchsandbox.com"&gt;hello@fetchsandbox.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Free during early access.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I know I'm not the only one who's been through this. UAT → staging → "just give them prod." If you've found a better way to handle partner sandbox environments, I'd genuinely like to know. Still figuring this out.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>apitesting</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>This is amazing truly</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Tue, 31 Mar 2026 00:53:27 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/this-is-amazing-truly-2agp</link>
      <guid>https://dev.to/fetchsandbox/this-is-amazing-truly-2agp</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/axrisi/stop-paying-for-cloud-deploy-a-highly-available-cluster-on-your-own-hardware-in-minutes-14f0" class="crayons-story__hidden-navigation-link"&gt;Stop Paying for Cloud: Deploy a Highly Available Cluster on Your Own Hardware in Minutes | MicroCloud&lt;/a&gt;
    &lt;div class="crayons-article__cover crayons-article__cover__image__feed"&gt;
      &lt;iframe src="https://www.youtube.com/embed/QL5K-hEcdOE" title="Stop Paying for Cloud: Deploy a Highly Available Cluster on Your Own Hardware in Minutes | MicroCloud"&gt;&lt;/iframe&gt;
    &lt;/div&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/axrisi" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F3226798%2F0c0a8594-658c-4146-a639-8068ede85f67.jpg" alt="axrisi profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/axrisi" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Nikoloz Turazashvili (@axrisi)
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Nikoloz Turazashvili (&lt;a class="mentioned-user" href="https://dev.to/axrisi"&gt;@axrisi&lt;/a&gt;)
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png" width="166" height="102"&gt;&lt;/a&gt;
              
              &lt;div id="story-author-preview-content-3409402" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/axrisi" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F3226798%2F0c0a8594-658c-4146-a639-8068ede85f67.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Nikoloz Turazashvili (@axrisi)&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/axrisi/stop-paying-for-cloud-deploy-a-highly-available-cluster-on-your-own-hardware-in-minutes-14f0" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 26&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/axrisi/stop-paying-for-cloud-deploy-a-highly-available-cluster-on-your-own-hardware-in-minutes-14f0" id="article-link-3409402"&gt;
          Stop Paying for Cloud: Deploy a Highly Available Cluster on Your Own Hardware in Minutes | MicroCloud
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/linux"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;linux&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/axrisi/stop-paying-for-cloud-deploy-a-highly-available-cluster-on-your-own-hardware-in-minutes-14f0" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;29&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/axrisi/stop-paying-for-cloud-deploy-a-highly-available-cluster-on-your-own-hardware-in-minutes-14f0#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              3&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>linux</category>
      <category>devops</category>
      <category>cloud</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My mock server lied to me. So I built a stateful API sandbox.</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Mon, 30 Mar 2026 23:26:50 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/my-mock-server-lied-to-me-so-i-built-a-stateful-api-sandbox-549n</link>
      <guid>https://dev.to/fetchsandbox/my-mock-server-lied-to-me-so-i-built-a-stateful-api-sandbox-549n</guid>
      <description>&lt;p&gt;Last month I was integrating with a payment API. Wrote my tests against a mock server, everything passed, shipped to staging — and the whole flow broke.&lt;/p&gt;

&lt;p&gt;The mock told me &lt;code&gt;POST /charges&lt;/code&gt; returns &lt;code&gt;{"id": "ch_123"}&lt;/code&gt;. And it does. But my code then called &lt;code&gt;GET /charges/ch_123&lt;/code&gt; to verify the status, and the mock returned 404. Because the mock doesn't actually &lt;em&gt;store&lt;/em&gt; anything. Every request lives in its own universe.&lt;/p&gt;

&lt;p&gt;I lost half a day to this. And it wasn't the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with stateless mocks
&lt;/h2&gt;

&lt;p&gt;I've used Prism, WireMock, Mockoon — they're solid tools. You point them at an OpenAPI spec and they generate responses. But the responses are canned. There's no memory between requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /customers → 201 &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;: &lt;span class="s2"&gt;"cust_123"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
GET /customers/cust_123 → 404   &lt;span class="c"&gt;# has no idea you just created this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine for unit tests where you're testing your HTTP client. It falls apart the moment you have a multi-step flow.&lt;/p&gt;

&lt;p&gt;Think about how a real Stripe integration works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a customer&lt;/li&gt;
&lt;li&gt;Create a payment intent &lt;em&gt;for that customer&lt;/em&gt; (needs the customer ID from step 1)&lt;/li&gt;
&lt;li&gt;Confirm the payment intent (needs the PI ID from step 2)&lt;/li&gt;
&lt;li&gt;A webhook fires (your server needs to handle it)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A mock server can't do steps 2-4. The IDs don't carry over. The webhook never fires. You're testing a fantasy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually needed
&lt;/h2&gt;

&lt;p&gt;I needed a sandbox where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;POST creates a real resource I can GET later&lt;/li&gt;
&lt;li&gt;IDs chain between requests like they would in production&lt;/li&gt;
&lt;li&gt;State transitions work (a charge goes from &lt;code&gt;pending&lt;/code&gt; to &lt;code&gt;succeeded&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Webhooks fire when things change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically — not a mock, but a tiny fake version of the actual API that behaves like the real thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built one
&lt;/h2&gt;

&lt;p&gt;I've been heads-down on &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; for a few months now. You give it an OpenAPI spec and it generates a stateful sandbox with seed data, state machines, and webhook events.&lt;/p&gt;

&lt;p&gt;Here's what it looks like from the terminal:&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; fetchsandbox

fetchsandbox generate ./stripe-openapi.yaml
&lt;span class="c"&gt;# ✓ Sandbox ready: 587 endpoints, 63 seed records&lt;/span&gt;

fetchsandbox run stripe &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;span class="c"&gt;# ✓ Accept a payment — 3/3 steps passed&lt;/span&gt;
&lt;span class="c"&gt;# ✓ Onboard a connected account — 3/3 steps passed&lt;/span&gt;
&lt;span class="c"&gt;# ✓ Respond to a dispute — 2/2 steps passed&lt;/span&gt;
&lt;span class="c"&gt;# ✓ All workflows passed — 3/3 (9ms)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;run --all&lt;/code&gt; command is the thing I wish I'd had. It executes every integration workflow end-to-end — creating resources, chaining IDs between steps, and verifying each response. If something breaks, you see exactly which step failed and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stuff that surprised me while building it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error scenarios were harder than happy paths.&lt;/strong&gt; I added a &lt;code&gt;--scenario&lt;/code&gt; flag so you can switch the whole sandbox to "auth_failure" mode and see what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fetchsandbox run stripe accept_payment &lt;span class="nt"&gt;--scenario&lt;/span&gt; auth_failure
&lt;span class="c"&gt;# ✗ Step 1: POST /v1/payment_intents → 401 Unauthorized&lt;/span&gt;
&lt;span class="c"&gt;# Scenario "auth_failure" correctly caused failure.&lt;/span&gt;
&lt;span class="c"&gt;# Scenario reset to default.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My code had a bug where it didn't handle 401 on the payment intent endpoint — only on the customer endpoint. Would never have caught that with a regular mock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks were a rabbit hole.&lt;/strong&gt; In a real Stripe integration, half the logic is in webhook handlers. The sandbox now fires webhook events when resources mutate, and you can watch them in real-time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fetchsandbox webhook-listen stripe
&lt;span class="c"&gt;# 12:04:31  payment_intent.created  pi_xyz  → requires_confirmation&lt;/span&gt;
&lt;span class="c"&gt;# 12:04:32  payment_intent.succeeded pi_xyz → succeeded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inspecting state is underrated.&lt;/strong&gt; After running a workflow, you can see exactly what's in the sandbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fetchsandbox state stripe customers
&lt;span class="c"&gt;# customers — 3 records&lt;/span&gt;
&lt;span class="c"&gt;# ┌──────────────┬─────────────────┬──────────┐&lt;/span&gt;
&lt;span class="c"&gt;# │ id           │ email           │ status   │&lt;/span&gt;
&lt;span class="c"&gt;# ├──────────────┼─────────────────┼──────────┤&lt;/span&gt;
&lt;span class="c"&gt;# │ cust_abc123  │ test@acme.com   │ active   │&lt;/span&gt;
&lt;span class="c"&gt;# └──────────────┴─────────────────┴──────────┘&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it compares to the alternatives
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend FetchSandbox replaces everything. Here's where I honestly think it sits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Mock server (Prism)&lt;/th&gt;
&lt;th&gt;Vendor sandbox (Stripe test mode)&lt;/th&gt;
&lt;th&gt;FetchSandbox&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;1 min&lt;/td&gt;
&lt;td&gt;15-30 min (account + keys)&lt;/td&gt;
&lt;td&gt;&amp;lt; 30 sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateful&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signup required&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works offline&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Hosted (offline coming)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matches prod exactly&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (schema-accurate, not logic-accurate)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (with CLI forwarding)&lt;/td&gt;
&lt;td&gt;Yes (built-in)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any OpenAPI spec&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Only their API&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest gap: FetchSandbox doesn't replicate vendor-specific business logic. Stripe's test mode knows that a card ending in 4242 succeeds and 4000000000000002 declines. FetchSandbox doesn't. It validates your &lt;em&gt;integration pattern&lt;/em&gt;, not the vendor's edge cases.&lt;/p&gt;

&lt;p&gt;For me, that's the right tradeoff. I use FetchSandbox while building the integration, then switch to the vendor's test mode for final validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD is where it clicks
&lt;/h2&gt;

&lt;p&gt;The thing I'm most excited about is this:&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="c1"&gt;# GitHub Actions&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prove integration works before deploy&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx fetchsandbox run stripe --all --json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 0 = all workflows pass. Exit code 1 = something broke. Your pipeline catches integration regressions before they hit staging.&lt;/p&gt;

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

&lt;p&gt;I ran a benchmark — time from "I want to explore this API" to "I made my first successful call":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vendor docs path:&lt;/strong&gt; 15-30 minutes (signup → dashboard → keys → local server → SDK → code → run)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FetchSandbox path:&lt;/strong&gt; under 1 second (open portal → endpoint is callable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://nordicapis.com/why-time-to-first-call-is-a-vital-api-metric/" rel="noopener noreferrer"&gt;Nordic APIs&lt;/a&gt; defines TTFC (time to first call) benchmarks: under 2 minutes is "Champion" tier. Over 10 minutes is a "Red Flag."&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;19 APIs are live right now — Stripe, GitHub, Twilio, WorkOS, OpenAI, DigitalOcean, and more. No signup needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;fetchsandbox.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's free during early access while I figure out what developers actually need from it. If you try it and something breaks or feels wrong, I genuinely want to know — I'm &lt;a href="https://x.com/fetchsandbox" rel="noopener noreferrer"&gt;@fetchsandbox&lt;/a&gt; on X.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Curious what other people's testing setups look like for third-party APIs. Do you mock everything? Use vendor test modes? Some hybrid? Drop a comment — I've been deep in this problem for months and I'm still learning.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>apitesting</category>
      <category>openapi</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
