<?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>Brownfield Slack alerts: a 6-minute guided MCP run on Stripe + Resend webhooks</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Wed, 27 May 2026 14:45:18 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/brownfield-slack-alerts-a-6-minute-guided-mcp-run-on-stripe-resend-webhooks-b4c</link>
      <guid>https://dev.to/fetchsandbox/brownfield-slack-alerts-a-6-minute-guided-mcp-run-on-stripe-resend-webhooks-b4c</guid>
      <description>&lt;p&gt;I recorded a &lt;strong&gt;raw ~6.5 minute&lt;/strong&gt; screen capture of my IDE running a full FetchSandbox MCP guided flow. No polish, no voiceover script — just what happens when you ask an agent to add &lt;strong&gt;Slack notifications for failed payments&lt;/strong&gt; in a repo that &lt;strong&gt;already&lt;/strong&gt; has Stripe checkout, Clerk JWT verification, and Resend receipt webhooks.&lt;/p&gt;

&lt;p&gt;The video:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;YOUTUBE_URL&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This post is the written version of that session, with the parts that are easier to scan in prose than in a screencast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ask (and why it is not a greenfield task)
&lt;/h2&gt;

&lt;p&gt;Prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add slack notifications for failed payments — should plug into the existing stripe + resend webhook handlers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: a small Next.js 15 + FastAPI demo (&lt;code&gt;stripe-checkout-demo&lt;/code&gt;) that had seen &lt;strong&gt;three prior integration sessions&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;What landed&lt;/th&gt;
&lt;th&gt;Where Slack attaches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stripe seed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /create-payment-intent&lt;/code&gt;, &lt;code&gt;POST /webhook&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;payment_intent.payment_failed&lt;/code&gt; branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clerk&lt;/td&gt;
&lt;td&gt;JWT dep, &lt;code&gt;POST /clerk-webhook&lt;/code&gt; (Svix)&lt;/td&gt;
&lt;td&gt;(out of scope for this alert)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;send_receipt()&lt;/code&gt;, &lt;code&gt;POST /resend-webhook&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;bounce/complaint branches + receipt try/except&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Slack is the &lt;strong&gt;fourth layer&lt;/strong&gt;. The agent cannot treat this like a tutorial repo with one file and one webhook. It has to find the exact lines where failures today only &lt;code&gt;print()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That repo reading is why a full guided run is &lt;strong&gt;~6–7 minutes&lt;/strong&gt; in the recording — not because MCP is slow, but because &lt;strong&gt;brownfield integration is mostly comprehension&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “guided” means in practice
&lt;/h2&gt;

&lt;p&gt;FetchSandbox MCP does not jump straight to &lt;code&gt;POST /chat.completions&lt;/code&gt; style codegen. The flow I used (fs-router skill) looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;intake → introspect repo → comprehend stack + failure surfaces → guide() routing → discovery questions → prove upstream contract → (then) propose changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1 — Introspect
&lt;/h3&gt;

&lt;p&gt;The introspect script scanned the repo and returned framework hints (Next.js ^15, FastAPI backend) and SDK signals for Stripe in &lt;code&gt;server/main.py&lt;/code&gt;. The intent string mentioned Resend too; Slack is not in the FetchSandbox brain catalog, so it never appears as a routable spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Comprehend (the load-bearing step)
&lt;/h3&gt;

&lt;p&gt;The agent catalogued &lt;strong&gt;where alerts would attach&lt;/strong&gt;, not a new &lt;code&gt;/slack-webhook&lt;/code&gt; route:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Today&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payment_intent.payment_failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;print()&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;webhook handler ~L95–98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;email.bounced&lt;/code&gt; / &lt;code&gt;email.complained&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;print()&lt;/code&gt; + TODO&lt;/td&gt;
&lt;td&gt;Resend handler ~L139–145&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receipt send raises (Resend SDK)&lt;/td&gt;
&lt;td&gt;swallowed with &lt;code&gt;print()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;inside Stripe success path ~L92–94&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The user named Stripe + Resend handlers. The comprehend step also flagged the &lt;strong&gt;receipt try/except&lt;/strong&gt; as a third visibility gap — correct webhook hygiene (do not fail Stripe retries) but operationally invisible without something like Slack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — &lt;code&gt;guide()&lt;/code&gt; routing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;guide(intent="add slack notifications for failed payments — plug into existing stripe + resend webhook handlers...")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;spec:&lt;/strong&gt; &lt;code&gt;stripe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;workflow:&lt;/strong&gt; &lt;code&gt;accept_payment&lt;/code&gt; (Tier-1 default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;confidence:&lt;/strong&gt; 0.85&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reasoning:&lt;/strong&gt; no &lt;code&gt;slack&lt;/code&gt; spec; Stripe is the upstream that emits the event Slack will format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That routing is the right call, but it needs to be said plainly: &lt;strong&gt;FetchSandbox proves the upstream event, not the Slack POST.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Discovery (failures tab matters)
&lt;/h3&gt;

&lt;p&gt;Multi-tab confirmation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Implies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Customer geo&lt;/td&gt;
&lt;td&gt;US only&lt;/td&gt;
&lt;td&gt;no 3DS scenario&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capture&lt;/td&gt;
&lt;td&gt;Auto on confirm&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accept_payment&lt;/code&gt; workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure modes&lt;/td&gt;
&lt;td&gt;Declined card (~60s)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;payment_declined&lt;/code&gt; scenario&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you skip the failures tab, you get a happy-path proof that does not exercise the webhook your Slack message cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Proof run (and the honest limit)
&lt;/h2&gt;

&lt;p&gt;Delegated to fs-prove-payments: &lt;code&gt;spec=stripe&lt;/code&gt;, &lt;code&gt;workflow=accept_payment&lt;/code&gt;, &lt;code&gt;scenario=payment_declined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sandbox import reused the bundled Stripe spec with hand-curated workflows. Run result on paper: &lt;strong&gt;6/6 steps passed&lt;/strong&gt;, webhooks verified: &lt;code&gt;payment_intent.created&lt;/code&gt;, &lt;code&gt;payment_intent.succeeded&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asked for&lt;/th&gt;
&lt;th&gt;Got&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;payment_declined&lt;/code&gt; → &lt;code&gt;payment_intent.payment_failed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Terminal &lt;code&gt;succeeded&lt;/code&gt;, success webhooks&lt;/td&gt;
&lt;td&gt;Curated &lt;code&gt;accept_payment&lt;/code&gt; uses a deterministic pass card; scenario hook exists in the engine but this Tier-1 workflow does not exercise it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same class of limit as other Stripe sessions: the &lt;strong&gt;catalog workflow&lt;/strong&gt; is a fast preflight, not a substitute for driving a real decline.&lt;/p&gt;

&lt;p&gt;To prove the handler your Slack code will sit in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;stripe listen --forward-to localhost:8000/webhook&lt;/code&gt; + card &lt;code&gt;4000000000000002&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Full PI flow → &lt;code&gt;payment_intent.payment_failed&lt;/code&gt; at your handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stripe trigger payment_intent.payment_failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast iteration on Slack message template from a realistic payload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the agent got right without me asking
&lt;/h2&gt;

&lt;p&gt;Five things worth stealing for your own brownfield prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No new endpoint&lt;/strong&gt; — side effects only at existing handler lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third target surfaced&lt;/strong&gt; — receipt-send swallow, not only the two webhooks the user named.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing limitation named&lt;/strong&gt; — upstream Stripe proof, Slack downstream on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency applied to Slack&lt;/strong&gt; — Stripe retries on 5xx; use &lt;code&gt;pi.id&lt;/code&gt; as dedupe key for duplicate alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend proof deferred&lt;/strong&gt; — one workflow per delegation; bounce/complaint shapes need a separate Resend run.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What was not done in this recording
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No Slack diff applied yet (proof handed back; propose-changes step not shown in the artifact).&lt;/li&gt;
&lt;li&gt;No Resend proof run for &lt;code&gt;email.bounced&lt;/code&gt; / &lt;code&gt;email.complained&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No Slack spec proof (none in catalog).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected next moves from the session:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Propose &lt;code&gt;payment_intent.payment_failed&lt;/code&gt; → Slack at L95–98 (&lt;code&gt;SLACK_WEBHOOK_URL&lt;/code&gt; env).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stripe trigger payment_intent.payment_failed&lt;/code&gt; before editing message text.&lt;/li&gt;
&lt;li&gt;Separate Resend proof for bounce/complaint templates.&lt;/li&gt;
&lt;li&gt;Ship payment-failed path first; Resend alerts in a follow-up PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  MCP setup (unchanged from shorter demos)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetchsandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetchsandbox-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor: &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;, restart IDE, enable server in Settings → MCP.&lt;/p&gt;

&lt;p&gt;Optional skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://fetchsandbox.com/skills/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Docs tell your agent what &lt;strong&gt;should&lt;/strong&gt; happen. Runnable workflows tell it what &lt;strong&gt;did&lt;/strong&gt; happen in a sandbox — and brownfield comprehend tells it &lt;strong&gt;where your code already handles failures&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For Slack-on-Stripe, the valuable proof is not “can we call chat.postMessage” in FetchSandbox. It is: &lt;strong&gt;when &lt;code&gt;payment_intent.payment_failed&lt;/code&gt; arrives, does your handler have the fields you need for an alert, and have you tested that path with a real or triggered event?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Try FetchSandbox: &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;https://fetchsandbox.com&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Stripe workflows: &lt;a href="https://fetchsandbox.com/docs/stripe" rel="noopener noreferrer"&gt;https://fetchsandbox.com/docs/stripe&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;Watch the raw demo: &lt;a href="https://youtu.be/AB36LXwfoTQ" rel="noopener noreferrer"&gt;https://youtu.be/AB36LXwfoTQ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have done brownfield API work with agents, I am curious: should the default flow always &lt;strong&gt;map existing handlers&lt;/strong&gt; before any proof run, or do you prefer prove-first on greenfield repos?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>stripe</category>
      <category>webhooks</category>
      <category>api</category>
    </item>
    <item>
      <title>Test Resend webhook reconciliation before production</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Fri, 22 May 2026 03:59:54 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-resend-webhook-reconciliation-before-production-19k5</link>
      <guid>https://dev.to/fetchsandbox/test-resend-webhook-reconciliation-before-production-19k5</guid>
      <description>&lt;p&gt;I was trying the Resend API for a small product flow, and the first &lt;code&gt;POST /emails&lt;/code&gt; call was not the part that worried me most.&lt;/p&gt;

&lt;p&gt;That call gives you an email ID.&lt;/p&gt;

&lt;p&gt;The part I care about is what happens later, when events come back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email.delivered
email.bounced
email.opened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resend has docs for these webhook events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;email.delivered&lt;/code&gt;: &lt;a href="https://resend.com/docs/webhooks/emails/delivered" rel="noopener noreferrer"&gt;https://resend.com/docs/webhooks/emails/delivered&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email.bounced&lt;/code&gt;: &lt;a href="https://resend.com/docs/webhooks/emails/bounced" rel="noopener noreferrer"&gt;https://resend.com/docs/webhooks/emails/bounced&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email.opened&lt;/code&gt;: &lt;a href="https://resend.com/docs/webhooks/emails/opened" rel="noopener noreferrer"&gt;https://resend.com/docs/webhooks/emails/opened&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs are useful. But the integration question is not just "what fields are in the payload?"&lt;/p&gt;

&lt;p&gt;The real question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;can my app map this event back to the right email record?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The small bug that becomes annoying later
&lt;/h2&gt;

&lt;p&gt;Most apps do something like this when sending email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create internal notification record
send email with Resend
store resend email id
wait for webhook events
update notification status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple enough.&lt;/p&gt;

&lt;p&gt;But the webhook arrives later, outside the request that sent the email.&lt;/p&gt;

&lt;p&gt;So now your app needs to answer a few things correctly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which internal record owns this Resend email ID?&lt;/li&gt;
&lt;li&gt;should &lt;code&gt;email.opened&lt;/code&gt; change the same status as &lt;code&gt;email.delivered&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;what happens if &lt;code&gt;email.bounced&lt;/code&gt; arrives after you already marked the email as sent?&lt;/li&gt;
&lt;li&gt;do you store the raw event payload for debugging?&lt;/li&gt;
&lt;li&gt;can you replay the event without creating duplicate state changes?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is hard in theory.&lt;/p&gt;

&lt;p&gt;It is just easy to skip until production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing only the send call is not enough
&lt;/h2&gt;

&lt;p&gt;If you test only this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /emails
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you have proven that Resend accepted the request.&lt;/p&gt;

&lt;p&gt;You have not proven your app can handle the lifecycle around that request.&lt;/p&gt;

&lt;p&gt;For a real product, I want to test this chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send email
store provider email id
receive delivered event
update internal status
receive opened or bounced event
reconcile final state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the integration.&lt;/p&gt;

&lt;p&gt;The endpoint is just one step inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is awkward to test early
&lt;/h2&gt;

&lt;p&gt;To test this against the real provider path, you usually need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resend API key
verified sender domain
public webhook endpoint
local tunnel
event logging
test recipient
some app state to update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That setup is reasonable before production.&lt;/p&gt;

&lt;p&gt;But when I am still exploring the API, I do not want to create all of that just to understand whether the workflow makes sense.&lt;/p&gt;

&lt;p&gt;I want to run the shape of the workflow first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow I wanted from my IDE
&lt;/h2&gt;

&lt;p&gt;At that point I wanted Cursor or Claude to run a small workflow for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send test email
capture email id
simulate delivered event
simulate opened or bounced event
verify app-facing state
show what changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried this through FetchSandbox MCP because it gives the agent a runnable API workflow instead of only docs to read.&lt;/p&gt;

&lt;p&gt;This is not replacing Resend's real environment. I would still need the real API key, real domain, production webhook endpoint, and provider limits before shipping.&lt;/p&gt;

&lt;p&gt;The point is earlier than that.&lt;/p&gt;

&lt;p&gt;Before wiring the real app, I wanted the agent to understand the lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request -&amp;gt; provider id -&amp;gt; webhook event -&amp;gt; internal state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What made the validation useful
&lt;/h2&gt;

&lt;p&gt;What helped was not a fake "success" message.&lt;/p&gt;

&lt;p&gt;It was seeing a small report closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS POST /emails returned email id
PASS internal record stored provider id
PASS email.delivered event matched provider id
PASS notification status changed to delivered
PASS email.opened event was recorded without duplicating the send
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if it fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAIL webhook event could not be matched to an internal record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the kind of failure I would rather see before production, not after users are asking why they never got an email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the workflow
&lt;/h2&gt;

&lt;p&gt;If you are exploring Resend, this is the workflow page I used:&lt;/p&gt;

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

&lt;p&gt;From an MCP client like Cursor or Claude, the ask is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate resend email workflow with fetchsandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow should not stop at &lt;code&gt;POST /emails -&amp;gt; 200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It should prove the lifecycle after the send call makes sense.&lt;/p&gt;

&lt;p&gt;That is where most integration bugs hide.&lt;/p&gt;

</description>
      <category>resend</category>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Run APIs before setup</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Wed, 20 May 2026 16:04:53 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/run-apis-before-setup-1k5o</link>
      <guid>https://dev.to/fetchsandbox/run-apis-before-setup-1k5o</guid>
      <description>&lt;p&gt;When I start integrating a new API, I do not want the first hour to be setup.&lt;/p&gt;

&lt;p&gt;I usually want to answer a simpler question first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;how does this workflow actually behave?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds obvious, but most API exploration still starts with a lot of prep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create an account
find or generate credentials
set local environment variables
copy a curl command
send one request
copy an ID from the response
paste it into the next request
check the provider dashboard
repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine when you already know the API.&lt;/p&gt;

&lt;p&gt;It is slow when you are still trying to understand the integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing I want is the workflow
&lt;/h2&gt;

&lt;p&gt;When I evaluate an API, the first request is rarely the full story.&lt;/p&gt;

&lt;p&gt;I want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what should I create?&lt;/li&gt;
&lt;li&gt;what comes back?&lt;/li&gt;
&lt;li&gt;which ID matters?&lt;/li&gt;
&lt;li&gt;what state changes after the request?&lt;/li&gt;
&lt;li&gt;what webhook or follow-up event should I expect?&lt;/li&gt;
&lt;li&gt;how do I know the workflow is actually complete?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Postman or curl can tell me whether one request returned &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But real integration work usually looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;choose API
run workflow
inspect response
follow state
verify final result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the part I want to make runnable before setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am building
&lt;/h2&gt;

&lt;p&gt;I am working on FetchSandbox MCP so developers can explore API workflows from their IDE.&lt;/p&gt;

&lt;p&gt;The goal is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;choose an API
ask Cursor or Claude to run the workflow
see the verified result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No local env first.&lt;/p&gt;

&lt;p&gt;No mock server stitching first.&lt;/p&gt;

&lt;p&gt;No copy-pasting IDs between five tabs just to understand the happy path.&lt;/p&gt;

&lt;p&gt;This is not meant to replace the real provider environment. Before shipping, you still need real credentials, real limits, production auth, webhook endpoints, monitoring, and all the normal engineering work.&lt;/p&gt;

&lt;p&gt;But before that, developers should be able to explore the workflow quickly.&lt;/p&gt;

&lt;p&gt;Especially now that coding agents are helping write integration code.&lt;/p&gt;

&lt;p&gt;Docs tell the agent what should happen.&lt;/p&gt;

&lt;p&gt;Runnable workflows let the agent prove what happened.&lt;/p&gt;

&lt;p&gt;That is the gap I keep seeing with APIs.&lt;/p&gt;

&lt;p&gt;The setup matters.&lt;/p&gt;

&lt;p&gt;But the workflow should come first.&lt;/p&gt;

</description>
      <category>api</category>
      <category>devtools</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Testing Resend is not just POST /emails</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Tue, 19 May 2026 13:25:37 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/testing-resend-is-not-just-post-emails-3mp0</link>
      <guid>https://dev.to/fetchsandbox/testing-resend-is-not-just-post-emails-3mp0</guid>
      <description>&lt;p&gt;I was trying to integrate Resend like an end user would.&lt;/p&gt;

&lt;p&gt;The first call was straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /emails
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You send the payload, get a &lt;code&gt;200&lt;/code&gt;, and now you have an email id.&lt;/p&gt;

&lt;p&gt;That part is not where most of the integration doubt comes from. The real questions start right after it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did it actually deliver?&lt;/li&gt;
&lt;li&gt;did it bounce?&lt;/li&gt;
&lt;li&gt;what events came back?&lt;/li&gt;
&lt;li&gt;how do I update my app state from those events?&lt;/li&gt;
&lt;li&gt;can I test that flow before wiring a real production setup?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Usually to experience all of that, you need more than just an API key. You need a domain, webhook endpoint, local tunnel, test users, event logs, and some way to replay or inspect what happened.&lt;/p&gt;

&lt;p&gt;That is a lot of setup just to understand the shape of the integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first request is not the workflow
&lt;/h2&gt;

&lt;p&gt;A successful &lt;code&gt;POST /emails&lt;/code&gt; only proves one thing: the API accepted the request.&lt;/p&gt;

&lt;p&gt;It does not prove your app understands the lifecycle around that email.&lt;/p&gt;

&lt;p&gt;For an email workflow, I want to see something closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send email
check response
get delivery status
trigger delivered/open/bounce events
verify final state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the part I wanted to run quickly from my IDE, while I was still exploring the API.&lt;/p&gt;

&lt;p&gt;Not after creating a full app. Not after configuring a real webhook route. Just enough workflow context to understand what needs to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Resend workflow from Claude or Cursor
&lt;/h2&gt;

&lt;p&gt;I added a Resend workflow to &lt;a href="https://fetchsandbox.com/install" rel="noopener noreferrer"&gt;FetchSandbox MCP&lt;/a&gt; so I could ask my IDE to validate the flow directly.&lt;/p&gt;

&lt;p&gt;Install once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; fetchsandbox-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then from Claude, Cursor, Codex, or any MCP client, ask something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate resend with fetchsandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful output is not just a fake success response. It is a small validation report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPLETE Send a transactional email

✓ Send email
  POST /emails -&amp;gt; 200

✓ Get email status
  GET /emails/{id} -&amp;gt; 200

✓ Webhook verification
  email.sent -&amp;gt; email.delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you a quick feel for the Resend workflow before you wire the real app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this helped me
&lt;/h2&gt;

&lt;p&gt;When I am integrating a new API, docs and SDK examples are helpful, but they usually stop at the first successful request.&lt;/p&gt;

&lt;p&gt;For email, payment, auth, calendar, CRM, and webhook-heavy APIs, that is not enough. The pain is usually in the next few steps.&lt;/p&gt;

&lt;p&gt;The endpoint works, but the product question is still open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;can my app handle the full state change?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why I like running provider workflows as acceptance checks from the IDE. It gives the agent a concrete path to execute instead of guessing from docs.&lt;/p&gt;

&lt;p&gt;For Resend, the workflow is simple on purpose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;send an email&lt;/li&gt;
&lt;li&gt;inspect the response&lt;/li&gt;
&lt;li&gt;check status&lt;/li&gt;
&lt;li&gt;simulate the events that matter&lt;/li&gt;
&lt;li&gt;verify the final state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Small loop, but it answers the thing I actually care about before production.&lt;/p&gt;

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

&lt;p&gt;The Resend workflow page is here:&lt;/p&gt;

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

&lt;p&gt;If you are exploring Resend from a greenfield app, try running the workflow from Claude or Cursor first. It is a faster way to understand what your app needs to handle after &lt;code&gt;POST /emails&lt;/code&gt; returns &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not replacing Resend's real sandbox or production setup. Just a quicker loop before you commit wiring, webhooks, and state handling.&lt;/p&gt;

</description>
      <category>resend</category>
      <category>webdev</category>
      <category>api</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Runnable API integration workflows from your favorite IDE</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Thu, 14 May 2026 13:05:00 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/runnable-api-integration-workflows-from-your-favorite-ide-4fc7</link>
      <guid>https://dev.to/fetchsandbox/runnable-api-integration-workflows-from-your-favorite-ide-4fc7</guid>
      <description>&lt;p&gt;AI coding agents are getting good at reading docs and writing integration code.&lt;/p&gt;

&lt;p&gt;But API integrations still fail in the workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create resource -&amp;gt; confirm state -&amp;gt; receive webhook -&amp;gt; fetch final state -&amp;gt; handle failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why I have been building FetchSandbox MCP: runnable API integration workflows from your favorite IDE.&lt;/p&gt;

&lt;p&gt;The goal is simple. If you are already working in Claude, Cursor, Codex, Continue, or another MCP-compatible IDE, your agent should be able to run the API workflow while it is helping you integrate it.&lt;/p&gt;

&lt;p&gt;Not just read the docs.&lt;/p&gt;

&lt;p&gt;Run the workflow.&lt;/p&gt;

&lt;p&gt;Inspect the response.&lt;/p&gt;

&lt;p&gt;Check the state transition.&lt;/p&gt;

&lt;p&gt;Verify the webhook behavior.&lt;/p&gt;

&lt;p&gt;Then use that proof while changing your app.&lt;/p&gt;

&lt;p&gt;Here is the raw demo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Iafkt01GAbA" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=Iafkt01GAbA&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you configure
&lt;/h2&gt;

&lt;p&gt;FetchSandbox MCP runs as a local MCP server using &lt;code&gt;npx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For Claude Desktop, add this to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cursor, add this to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.cursor/mcp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Claude Code, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/settings.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or a project-level &lt;code&gt;.mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The config is the same basic shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetchsandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetchsandbox-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding it, restart your IDE. In Cursor, also enable the new &lt;code&gt;fetchsandbox&lt;/code&gt; MCP server in Settings -&amp;gt; MCP Servers.&lt;/p&gt;

&lt;p&gt;You can also install FetchSandbox trigger-phrase skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://fetchsandbox.com/skills/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your IDE can respond to phrases like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate this integration with fetchsandbox
fs validate
check api integration coverage
validate my stripe integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you can ask your IDE
&lt;/h2&gt;

&lt;p&gt;Once the MCP server is connected, you can ask practical integration questions, not just documentation questions.&lt;/p&gt;

&lt;p&gt;For an existing app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use FetchSandbox to validate this Stripe integration. Run the available workflows, compare the expected terminal state and webhook events, then tell me what code paths in this repo need changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a new integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want to add a Stripe payment flow to this app. Use FetchSandbox to discover the runnable workflows first, then guide the implementation step by step.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run the checkout/payment workflow in FetchSandbox and inspect the failing step. Compare the expected response, state transition, and webhook events with my current implementation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For coverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check API integration coverage with FetchSandbox and write a markdown report I can commit to this PR.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last flow is important. The agent can produce a local artifact like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.fetchsandbox/validation-&amp;lt;date&amp;gt;.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with workflow results, step traces, acceptance criteria, terminal states, required webhook events, and invariants.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is different from a mock server
&lt;/h2&gt;

&lt;p&gt;A mock server can answer a request.&lt;/p&gt;

&lt;p&gt;A guided workflow answers a better question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Did the integration behavior actually work end to end?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a payment workflow is not just &lt;code&gt;POST /payment_intents&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create customer
create payment intent
confirm payment
capture payment
verify webhook
retrieve final state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your agent only sees endpoint examples, it can still generate code that compiles but misses the lifecycle.&lt;/p&gt;

&lt;p&gt;If your agent can run the workflow, it has execution context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FetchSandbox fits
&lt;/h2&gt;

&lt;p&gt;FetchSandbox turns OpenAPI specs into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stateful sandboxes&lt;/li&gt;
&lt;li&gt;realistic seed data&lt;/li&gt;
&lt;li&gt;lifecycle state machines&lt;/li&gt;
&lt;li&gt;auth simulation&lt;/li&gt;
&lt;li&gt;guided workflow runners&lt;/li&gt;
&lt;li&gt;webhook behavior&lt;/li&gt;
&lt;li&gt;generated developer portals&lt;/li&gt;
&lt;li&gt;MCP tools for IDE agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to replace final provider sandbox testing.&lt;/p&gt;

&lt;p&gt;The point is to give developers and agents a fast preflight loop while the integration is being built.&lt;/p&gt;

&lt;p&gt;Your agent can ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what workflows exist?
run this workflow
what state changed?
which webhook was expected?
what failed?
what should I change in the app?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the developer experience I want: guided API integration workflows from the IDE you already use.&lt;/p&gt;

&lt;p&gt;Try FetchSandbox:&lt;/p&gt;

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

&lt;p&gt;Watch the demo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Iafkt01GAbA" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=Iafkt01GAbA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build API integrations, I would love feedback on the developer flow: should agents be able to run guided API workflows directly from the IDE?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>openapi</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Test Cal.com booking flows without calendar chaos</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Tue, 12 May 2026 13:45:00 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-calcom-booking-flows-without-calendar-chaos-2d2c</link>
      <guid>https://dev.to/fetchsandbox/test-calcom-booking-flows-without-calendar-chaos-2d2c</guid>
      <description>&lt;p&gt;Scheduling integrations do not usually break on the first request.&lt;/p&gt;

&lt;p&gt;They break after the booking exists.&lt;/p&gt;

&lt;p&gt;The user gets an email. The host has Google Calendar connected. Your app sends its own ICS file. Then somebody reschedules or cancels and suddenly there are two calendar events, or one event refuses to disappear.&lt;/p&gt;

&lt;p&gt;That is not a generic "calendar APIs are hard" problem.&lt;/p&gt;

&lt;p&gt;It is a workflow identity problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The specific workflow
&lt;/h2&gt;

&lt;p&gt;The narrow Cal.com flow I care about is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get available slots&lt;/li&gt;
&lt;li&gt;Create a booking&lt;/li&gt;
&lt;li&gt;Fetch the booking&lt;/li&gt;
&lt;li&gt;Reschedule it&lt;/li&gt;
&lt;li&gt;Cancel it&lt;/li&gt;
&lt;li&gt;Verify the calendar and webhook side effects still point at the same booking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In API terms, the happy path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET  /v2/slots
POST /v2/bookings
GET  /v2/bookings/{bookingUid}
POST /v2/bookings/{bookingUid}/reschedule
POST /v2/bookings/{bookingUid}/cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the test.&lt;/p&gt;

&lt;p&gt;Not just "can I create a booking?"&lt;/p&gt;

&lt;p&gt;The real question is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slot -&amp;gt; booking -&amp;gt; calendar event -&amp;gt; reschedule -&amp;gt; cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do all of those still refer to the same thing?&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug shape
&lt;/h2&gt;

&lt;p&gt;Here is the kind of bug this flow catches.&lt;/p&gt;

&lt;p&gt;Your app creates a booking through Cal.com. The attendee has a connected Google Calendar. Cal.com syncs the event into Google.&lt;/p&gt;

&lt;p&gt;But your app also sends custom emails with its own ICS attachment.&lt;/p&gt;

&lt;p&gt;If the ICS UID from the Booking API and the synced Google Calendar event identity do not line up, the user can end up with two events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event from Cal.com sync
event from your custom ICS invite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then cancellation gets messy because your app cancels one identity while the other one remains in the calendar.&lt;/p&gt;

&lt;p&gt;This is the kind of integration bug that is easy to miss if you only test &lt;code&gt;POST /bookings&lt;/code&gt; in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would assert
&lt;/h2&gt;

&lt;p&gt;For a scheduling API, I want the test to preserve identity across the whole lifecycle.&lt;/p&gt;

&lt;p&gt;The assertions should be closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;booking created -&amp;gt; booking uid exists
calendar reference exists -&amp;gt; external event id is attached
reschedule uses same booking identity
cancel removes or updates the same calendar event
webhook events describe the same lifecycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if your product sends custom ICS files, there is one extra check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Booking API iCalUID matches the calendar event your user will update/cancel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last part is where teams usually learn the hard way that a "successful booking" does not mean a safe scheduling integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why static mocks miss it
&lt;/h2&gt;

&lt;p&gt;A static mock can return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /bookings -&amp;gt; 201
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not enough.&lt;/p&gt;

&lt;p&gt;It cannot easily prove that a later reschedule call still points at the booking created two steps earlier. It cannot prove that your cancellation code is updating the same calendar event your invite email created. It cannot prove that webhook handlers and calendar references agree about the booking identity.&lt;/p&gt;

&lt;p&gt;The state between calls is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FetchSandbox fits
&lt;/h2&gt;

&lt;p&gt;FetchSandbox has a Cal.com sandbox flow for the booking lifecycle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/cal-com/test-booking-lifecycle-locally" rel="noopener noreferrer"&gt;https://fetchsandbox.com/cal-com/test-booking-lifecycle-locally&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The broader Cal.com sandbox is here:&lt;/p&gt;

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

&lt;p&gt;The workflow is intentionally narrow because that is where integration bugs hide: slots, booking creation, booking lookup, reschedule, cancel, and the events around those changes.&lt;/p&gt;

&lt;p&gt;You can also connect your IDE to FetchSandbox through MCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetchsandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetchsandbox-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask an agent something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use FetchSandbox MCP to run the Cal.com booking lifecycle workflow.
Explain which identifiers my app should preserve across create, reschedule, cancel, and calendar sync.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the direction I think API testing should move:&lt;/p&gt;

&lt;p&gt;not just docs,&lt;br&gt;
not just static responses,&lt;br&gt;
but runnable workflow behavior from inside the developer's own environment.&lt;/p&gt;

&lt;p&gt;For Cal.com-style integrations, the useful question is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can my app create, move, and cancel a booking without leaving calendar ghosts behind?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That should be testable before production users find it.&lt;/p&gt;

</description>
      <category>calcom</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Test Notion database queries without a workspace</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Mon, 11 May 2026 23:44:45 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-notion-database-queries-without-a-workspace-56eh</link>
      <guid>https://dev.to/fetchsandbox/test-notion-database-queries-without-a-workspace-56eh</guid>
      <description>&lt;p&gt;Testing a Notion database query sounds simple until you try to do it in a real workspace.&lt;/p&gt;

&lt;p&gt;You need a workspace. You need an integration token. You need to share the right database with that integration. You need seed pages with the right properties. Then you run the query and hope you did not pollute somebody's actual team dashboard with test rows.&lt;/p&gt;

&lt;p&gt;The painful part is not &lt;code&gt;POST /v1/databases/{database_id}/query&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The painful part is proving the whole shape around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;does the database exist?&lt;/li&gt;
&lt;li&gt;do the properties match what your app expects?&lt;/li&gt;
&lt;li&gt;does the filter return the right rows?&lt;/li&gt;
&lt;li&gt;does the response shape match the code path you are about to ship?&lt;/li&gt;
&lt;li&gt;can you repeat the test without cleaning up a real Notion workspace?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The specific workflow
&lt;/h2&gt;

&lt;p&gt;The narrow workflow I care about here is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieve a Notion database&lt;/li&gt;
&lt;li&gt;Query that database for high-priority in-progress tasks&lt;/li&gt;
&lt;li&gt;Verify the returned page shape before wiring it into an app&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In FetchSandbox, the workflow is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET  /v1/databases/db-a1b2c3d4-e5f6-7890-abcd-ef1234567890
POST /v1/databases/db-a1b2c3d4-e5f6-7890-abcd-ef1234567890/query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query body filters by two properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"and"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"property"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"equals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Progress"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"property"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Priority"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"select"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"equals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"High"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the kind of test I want before touching production code.&lt;/p&gt;

&lt;p&gt;Not "does the endpoint return 200?"&lt;/p&gt;

&lt;p&gt;More like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database exists -&amp;gt; properties match -&amp;gt; filter runs -&amp;gt; rows come back -&amp;gt; app can trust the shape
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why docs and mocks miss this
&lt;/h2&gt;

&lt;p&gt;Docs usually show the endpoint and a sample payload.&lt;/p&gt;

&lt;p&gt;That helps, but it does not answer the messy integration questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happens when the database is missing?&lt;/li&gt;
&lt;li&gt;what if the integration does not have access?&lt;/li&gt;
&lt;li&gt;what if the property name is different from the sample?&lt;/li&gt;
&lt;li&gt;what if the query returns zero pages?&lt;/li&gt;
&lt;li&gt;what fields should my app treat as stable?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generic mocks usually miss this too because they return static JSON. They do not behave like a workspace with pages, databases, users, blocks, comments, and state.&lt;/p&gt;

&lt;p&gt;For a Notion integration, state matters.&lt;/p&gt;

&lt;p&gt;If your app reads tasks from Notion, the test needs to prove more than the request syntax. It needs to prove that your app can survive the workspace shape it expects.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I would test it
&lt;/h2&gt;

&lt;p&gt;For this workflow, I would test three cases:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Happy path
&lt;/h3&gt;

&lt;p&gt;The database exists and contains matching pages.&lt;/p&gt;

&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET database -&amp;gt; 200
POST query -&amp;gt; 200
matching pages returned
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This proves the app can read the expected database shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Permission denied
&lt;/h3&gt;

&lt;p&gt;The database exists, but the integration should not be allowed to read it.&lt;/p&gt;

&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST query -&amp;gt; 403
app shows setup guidance instead of crashing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the case most teams discover only after connecting a real workspace with incomplete permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Empty result
&lt;/h3&gt;

&lt;p&gt;The query is valid, but no pages match &lt;code&gt;Status = In Progress&lt;/code&gt; and &lt;code&gt;Priority = High&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST query -&amp;gt; 200
results: []
app shows an empty state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches a surprising number of UI and sync bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FetchSandbox fits
&lt;/h2&gt;

&lt;p&gt;This is the kind of workflow a stateful API sandbox should prove.&lt;/p&gt;

&lt;p&gt;With the Notion sandbox in FetchSandbox, you can test pages, databases, blocks, users, comments, and search without setting up a real workspace or using a real integration token.&lt;/p&gt;

&lt;p&gt;I recorded a raw walkthrough of this Notion database workflow here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/watch?v=xJpJ5dOR_Co" rel="noopener noreferrer"&gt;https://youtube.com/watch?v=xJpJ5dOR_Co&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The more interesting path is connecting your IDE to it.&lt;/p&gt;

&lt;p&gt;FetchSandbox MCP lets Cursor, Claude, or another MCP-capable agent discover and run these workflows from your own development environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"fetchsandbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetchsandbox-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can ask the agent something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use FetchSandbox MCP to explore the Notion sandbox.
Run the database query workflow and explain what response shape my app should handle.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changes the workflow from "read docs and copy examples" to "run the integration behavior from inside the IDE."&lt;/p&gt;

&lt;p&gt;The Notion portal is here:&lt;/p&gt;

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

&lt;p&gt;And the Notion landing page is here:&lt;/p&gt;

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

&lt;p&gt;The goal is not to replace Notion.&lt;/p&gt;

&lt;p&gt;The goal is to give developers a runnable place to understand the API lifecycle before they wire it into a real customer workspace.&lt;/p&gt;

&lt;p&gt;For this one workflow, the question is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can my app query a Notion database and handle the response shape correctly?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That should be testable before production.&lt;/p&gt;

</description>
      <category>notion</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>When your SaaS API has docs but no real sandbox</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Fri, 08 May 2026 18:43:53 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/when-your-saas-api-has-docs-but-no-real-sandbox-3mfh</link>
      <guid>https://dev.to/fetchsandbox/when-your-saas-api-has-docs-but-no-real-sandbox-3mfh</guid>
      <description>&lt;p&gt;A lot of small SaaS APIs have decent docs.&lt;/p&gt;

&lt;p&gt;Some even have an OpenAPI spec.&lt;/p&gt;

&lt;p&gt;But no real sandbox.&lt;/p&gt;

&lt;p&gt;That sounds fine until a developer tries to integrate the API and needs to test the part that docs cannot prove:&lt;/p&gt;

&lt;p&gt;Does the workflow actually hold together?&lt;/p&gt;

&lt;p&gt;Not one request.&lt;/p&gt;

&lt;p&gt;The workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not the first 200
&lt;/h2&gt;

&lt;p&gt;Most API docs can show this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /customers
→ 201 Created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful, but it is not enough.&lt;/p&gt;

&lt;p&gt;For a real SaaS integration, the next questions are usually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can I read the same customer back?
Does the subscription attach to the right customer?
Does the webhook fire?
Does my app know which user should get access?
What happens when the subscription is canceled?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where a docs-only API starts to feel thin.&lt;/p&gt;

&lt;p&gt;The integration does not fail because the endpoint is unknown.&lt;/p&gt;

&lt;p&gt;It fails because the lifecycle is not testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow small SaaS teams should expose
&lt;/h2&gt;

&lt;p&gt;If I were building a small SaaS API, the first sandbox workflow I would expose is boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /customers
  → create a customer

GET /customers/{id}
  → verify the customer exists

POST /subscriptions
  → attach a plan to that customer

GET /subscriptions/{id}
  → verify state is active or trialing

webhook: subscription.created
  → let the developer test access control

PATCH /subscriptions/{id}
  → cancel or pause

webhook: subscription.canceled
  → verify access gets removed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That workflow is not flashy.&lt;/p&gt;

&lt;p&gt;But it answers the question every integration needs to answer:&lt;/p&gt;

&lt;p&gt;Can my app keep its local state aligned with the API provider's state?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why examples are not enough
&lt;/h2&gt;

&lt;p&gt;Example responses are static.&lt;/p&gt;

&lt;p&gt;They can show what a customer looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cus_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buyer@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But they cannot prove the next request will remember it.&lt;/p&gt;

&lt;p&gt;They cannot prove a subscription belongs to that customer.&lt;/p&gt;

&lt;p&gt;They cannot prove the webhook event is shaped the same way as the API response.&lt;/p&gt;

&lt;p&gt;They cannot prove your app removes access when the subscription changes.&lt;/p&gt;

&lt;p&gt;That is not a docs problem. Docs are still needed.&lt;/p&gt;

&lt;p&gt;It is a sandbox problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;The most common failure is not dramatic.&lt;/p&gt;

&lt;p&gt;It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer create works
subscription create works
webhook handler returns 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then two weeks later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;paid customer has no access
canceled customer still has access
support cannot map billing ID to app user
webhook retry created duplicate state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All the individual calls looked fine.&lt;/p&gt;

&lt;p&gt;The workflow was never tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why small SaaS APIs skip sandboxes
&lt;/h2&gt;

&lt;p&gt;I understand why this happens.&lt;/p&gt;

&lt;p&gt;A small SaaS team is usually trying to ship the core product first.&lt;/p&gt;

&lt;p&gt;Building a second environment with fake accounts, seeded data, fake billing states, webhook retries, and lifecycle transitions is a lot of work.&lt;/p&gt;

&lt;p&gt;So the API ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;docs&lt;/li&gt;
&lt;li&gt;examples&lt;/li&gt;
&lt;li&gt;maybe an OpenAPI file&lt;/li&gt;
&lt;li&gt;maybe test credentials&lt;/li&gt;
&lt;li&gt;maybe a staging workspace if you ask support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That helps.&lt;/p&gt;

&lt;p&gt;But for developers integrating your API, the missing piece is still the same:&lt;/p&gt;

&lt;p&gt;Can I safely run the lifecycle before touching production?&lt;/p&gt;

&lt;h2&gt;
  
  
  A better minimum sandbox
&lt;/h2&gt;

&lt;p&gt;The minimum useful sandbox does not need to simulate your whole company.&lt;/p&gt;

&lt;p&gt;It only needs to simulate the workflow developers are scared to test in production.&lt;/p&gt;

&lt;p&gt;For a SaaS API, that might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer → subscription → webhook → access change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an email API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;template → send → delivered/bounced webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a CRM API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lead → contact → deal → status webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a CI API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipeline → workflow → job → failed/rerun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is the same:&lt;/p&gt;

&lt;p&gt;State has to move.&lt;/p&gt;

&lt;p&gt;The next request has to see the previous request.&lt;/p&gt;

&lt;p&gt;The webhook has to match the state change.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I am thinking about this in FetchSandbox
&lt;/h2&gt;

&lt;p&gt;This is one of the reasons I am building FetchSandbox around workflows, not just endpoints.&lt;/p&gt;

&lt;p&gt;An OpenAPI spec can tell you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /customers exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But an integration needs to know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /customers
GET /customers/{id}
POST /subscriptions
webhook subscription.created
PATCH /subscriptions/{id}
webhook subscription.canceled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between an endpoint mock and an integration sandbox.&lt;/p&gt;

&lt;p&gt;For small SaaS teams, I think this could be a lightweight way to give developers something close to a sandbox without building a full second production environment.&lt;/p&gt;

&lt;p&gt;Start with one scary workflow.&lt;/p&gt;

&lt;p&gt;Make it stateful.&lt;/p&gt;

&lt;p&gt;Fire the webhook.&lt;/p&gt;

&lt;p&gt;Let developers break it safely.&lt;/p&gt;

&lt;p&gt;That is already much better than docs alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=small_saas_no_sandbox" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; is where I am testing this idea: turn an OpenAPI spec into a stateful sandbox with workflows developers can run before production.&lt;/p&gt;

</description>
      <category>api</category>
      <category>saas</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Polar external_id reconciliation: the customer field that keeps checkout tied to your user</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Thu, 07 May 2026 17:57:02 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/polar-externalid-reconciliation-the-customer-field-that-keeps-checkout-tied-to-your-user-5e1c</link>
      <guid>https://dev.to/fetchsandbox/polar-externalid-reconciliation-the-customer-field-that-keeps-checkout-tied-to-your-user-5e1c</guid>
      <description>&lt;p&gt;Checkout is not where a billing integration starts.&lt;/p&gt;

&lt;p&gt;For a SaaS app, it usually starts one step earlier:&lt;/p&gt;

&lt;p&gt;Can this billing customer be tied back to the right user in my app?&lt;/p&gt;

&lt;p&gt;That tiny mapping is easy to ignore when you are just trying to get checkout working. But later, when a webhook comes in, or a subscription changes, or support asks why someone paid but still has no access, this is the field you wish you tested earlier.&lt;/p&gt;

&lt;p&gt;In Polar, that mapping can live in customer metadata / external customer identifiers like &lt;code&gt;external_id&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug is not dramatic
&lt;/h2&gt;

&lt;p&gt;The bug usually looks boring.&lt;/p&gt;

&lt;p&gt;Your app creates a customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/customers/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back a Polar customer ID.&lt;/p&gt;

&lt;p&gt;Then later your app receives something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer.created
subscription.active
order.paid
benefit.granted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your code has to answer one question:&lt;/p&gt;

&lt;p&gt;Which local user does this belong to?&lt;/p&gt;

&lt;p&gt;If you only stored the provider customer ID, you can probably make it work. But if your local user ID never made it into the customer record, or your webhook handler assumes the mapping exists before it actually does, the access-control code gets messy fast.&lt;/p&gt;

&lt;p&gt;That is where a field like &lt;code&gt;external_id&lt;/code&gt; matters.&lt;/p&gt;

&lt;p&gt;It is not just extra metadata. It is the bridge between billing state and product state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small workflow I want to test
&lt;/h2&gt;

&lt;p&gt;Before touching hosted checkout, I want this workflow to pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/customers/
  email: buyer@example.com
  name: Test Buyer
  external_id: user_123

GET /v1/customers/{id}
  verify id exists
  verify email survived
  verify external_id is still user_123

customer.created
  verify webhook can be reconciled back to user_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it.&lt;/p&gt;

&lt;p&gt;No payment.&lt;br&gt;
No subscription access yet.&lt;br&gt;
No real customer.&lt;/p&gt;

&lt;p&gt;Just proving that the customer record your app creates can be read back and matched to the user who started the flow.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this catches real billing bugs
&lt;/h2&gt;

&lt;p&gt;Most checkout demos focus on the happy path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;click checkout&lt;/li&gt;
&lt;li&gt;pay&lt;/li&gt;
&lt;li&gt;redirect back&lt;/li&gt;
&lt;li&gt;unlock access&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But production systems depend on slower, less visible steps.&lt;/p&gt;

&lt;p&gt;Your webhook might arrive before your app finishes saving local state. Your user may open checkout in one tab and close it. A retry may deliver the same event twice. Your support tooling may need to search by internal user ID, not provider ID.&lt;/p&gt;

&lt;p&gt;If the customer mapping is weak, all of those paths become harder.&lt;/p&gt;

&lt;p&gt;The failure usually shows up as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user paid but access is not granted&lt;/li&gt;
&lt;li&gt;webhook event cannot find a local user&lt;/li&gt;
&lt;li&gt;customer exists in Polar, but your app has no clean link to it&lt;/li&gt;
&lt;li&gt;support sees a billing ID but cannot connect it to an account&lt;/li&gt;
&lt;li&gt;tests pass because they only checked &lt;code&gt;201 Created&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The endpoint returned success. The integration still does not know who owns the customer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why a static mock is not enough
&lt;/h2&gt;

&lt;p&gt;A static mock can return this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cus_test_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buyer@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"external_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the next request is the important one.&lt;/p&gt;

&lt;p&gt;If you call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /v1/customers/cus_test_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does the mock remember the customer you just created?&lt;/p&gt;

&lt;p&gt;Does it preserve &lt;code&gt;external_id&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;Can your webhook handler use that same value to reconcile the event?&lt;/p&gt;

&lt;p&gt;For billing integrations, state matters more than the first response. A fake &lt;code&gt;201&lt;/code&gt; is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it in FetchSandbox
&lt;/h2&gt;

&lt;p&gt;We added Polar to FetchSandbox so this kind of pre-checkout state can be tested without a Polar token.&lt;/p&gt;

&lt;p&gt;The useful test is not "can I call the endpoint?"&lt;/p&gt;

&lt;p&gt;It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/customers/
GET  /v1/customers/{id}
inspect customer.created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check whether the same customer state flows through each step.&lt;/p&gt;

&lt;p&gt;That gives you a simple confidence check before wiring checkout, subscriptions, orders, and benefits.&lt;/p&gt;

&lt;p&gt;It does not replace Polar's real sandbox. You still need that before launch for hosted checkout, real signatures, account configuration, and final payment behavior.&lt;/p&gt;

&lt;p&gt;But it helps catch the boring mapping problem earlier.&lt;/p&gt;

&lt;p&gt;And boring mapping problems are exactly the ones that become painful after someone pays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/polar?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=polar_external_id" rel="noopener noreferrer"&gt;Try the Polar sandbox&lt;/a&gt; — no Polar token needed.&lt;/p&gt;

&lt;p&gt;Curious how others handle this. Do you store provider customer IDs only, or always write your internal user ID into the billing customer too?&lt;/p&gt;

</description>
      <category>polar</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Test Polar's customer and product API flow without an API token</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Tue, 05 May 2026 14:08:40 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-polars-customer-and-product-api-flow-without-an-api-token-3doh</link>
      <guid>https://dev.to/fetchsandbox/test-polars-customer-and-product-api-flow-without-an-api-token-3doh</guid>
      <description>&lt;p&gt;&lt;em&gt;Polar billing integrations do not start at checkout.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first real question is usually simpler:&lt;/p&gt;

&lt;p&gt;Can my app create the customer and product records it will depend on later?&lt;/p&gt;

&lt;p&gt;If that part is shaky, checkout and subscription handling get harder to trust. You end up debugging billing logic when the actual bug was earlier: the customer ID did not persist, the product did not show up in the list call, or your app stored an internal user ID that never made it into the billing system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring part is the important part
&lt;/h2&gt;

&lt;p&gt;Polar's API has the objects you expect in a modern billing system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customers&lt;/li&gt;
&lt;li&gt;products&lt;/li&gt;
&lt;li&gt;checkouts&lt;/li&gt;
&lt;li&gt;orders&lt;/li&gt;
&lt;li&gt;subscriptions&lt;/li&gt;
&lt;li&gt;benefits&lt;/li&gt;
&lt;li&gt;webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checkout flow is the part everyone thinks about. But checkout depends on earlier state.&lt;/p&gt;

&lt;p&gt;For example, a product has to exist before you can sell it. A customer has to map back to your own user model before you can reconcile access later. Polar supports this through fields like &lt;code&gt;external_id&lt;/code&gt; / external customer IDs, which are exactly the sort of small integration detail that becomes painful if you do not test it early.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first workflow to prove
&lt;/h2&gt;

&lt;p&gt;The first Polar workflow I want to trust is not payment. It is state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/customers/
  → create a customer with email, name, external_id
  → customer.created webhook fires

GET /v1/customers/{id}
  → read the same customer back
  → verify the ID and external_id survived
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for products:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/products/
  → create a product with price data
  → product.created webhook fires

GET /v1/products/
  → verify the product appears in the catalog list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a flashy workflow. It is the foundation.&lt;/p&gt;

&lt;p&gt;If your app cannot round-trip customer and product state, you do not have a billing integration yet. You have a few API calls that happened to return 201.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why mocks are weak here
&lt;/h2&gt;

&lt;p&gt;A static mock can return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cus_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-buyer@example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the next request is where the truth shows up.&lt;/p&gt;

&lt;p&gt;If you call &lt;code&gt;GET /v1/customers/cus_123&lt;/code&gt;, does the mock know about the customer you just created? Does it preserve your &lt;code&gt;external_id&lt;/code&gt;? Does the list endpoint include the product you created one step earlier?&lt;/p&gt;

&lt;p&gt;Most mocks do not. Every request lives alone.&lt;/p&gt;

&lt;p&gt;For billing APIs, that is a bad tradeoff because the whole integration is stateful. Customers, products, checkouts, orders, subscriptions, and benefits are related. Your code is not just calling endpoints. It is moving through a lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Polar in FetchSandbox
&lt;/h2&gt;

&lt;p&gt;We onboarded Polar into FetchSandbox today.&lt;/p&gt;

&lt;p&gt;The current sandbox includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Polar OpenAPI spec support&lt;/li&gt;
&lt;li&gt;seeded customers, products, and subscriptions&lt;/li&gt;
&lt;li&gt;stateful customer creation and read-back&lt;/li&gt;
&lt;li&gt;stateful product creation and list verification&lt;/li&gt;
&lt;li&gt;webhook events like &lt;code&gt;customer.created&lt;/code&gt; and &lt;code&gt;product.created&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;subscription states like &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;past_due&lt;/code&gt;, &lt;code&gt;canceled&lt;/code&gt;, &lt;code&gt;revoked&lt;/code&gt;, &lt;code&gt;trialing&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can run the customer workflow without a Polar token, without creating a Polar organization, and without touching real payment state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/customers/
GET  /v1/customers/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or test product creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /v1/products/
GET  /v1/products/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is not that the first call returns a successful response. The useful part is that the second call can prove the first one changed state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this catches early
&lt;/h2&gt;

&lt;p&gt;This kind of preflight catches boring bugs before they become billing bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your app loses the billing customer ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;external_id&lt;/code&gt; does not map cleanly back to your user&lt;/li&gt;
&lt;li&gt;product creation succeeds but your catalog query does not find it&lt;/li&gt;
&lt;li&gt;your webhook handler assumes a customer exists before the create event is processed&lt;/li&gt;
&lt;li&gt;your tests only check status codes, not persisted state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are dramatic. They are just the kind of bugs that waste an afternoon once checkout and subscriptions are already wired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the real Polar sandbox still matters
&lt;/h2&gt;

&lt;p&gt;This is not a replacement for testing against Polar's own sandbox before launch.&lt;/p&gt;

&lt;p&gt;You still want the real environment for final payment behavior, account-specific configuration, hosted checkout, signatures, and anything that depends on Polar's production logic.&lt;/p&gt;

&lt;p&gt;The point is earlier than that.&lt;/p&gt;

&lt;p&gt;Before you do final validation, you should be able to prove your app understands the basic lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create billing object&lt;/li&gt;
&lt;li&gt;Store returned ID&lt;/li&gt;
&lt;li&gt;Read it back&lt;/li&gt;
&lt;li&gt;React to the webhook&lt;/li&gt;
&lt;li&gt;Keep your local state aligned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the layer FetchSandbox is good at.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/polar?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=polar_launch" rel="noopener noreferrer"&gt;Try the Polar sandbox&lt;/a&gt; — no Polar token needed.&lt;/p&gt;

&lt;p&gt;Curious how other teams test billing integrations before real payment flows. Do you start with the provider sandbox immediately, or do you preflight the API state first?&lt;/p&gt;

</description>
      <category>polar</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to test Twilio message delivery failures locally</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Fri, 01 May 2026 13:48:32 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/how-to-test-twilio-message-delivery-failures-locally-1gac</link>
      <guid>https://dev.to/fetchsandbox/how-to-test-twilio-message-delivery-failures-locally-1gac</guid>
      <description>&lt;p&gt;&lt;em&gt;Twilio usually gives you the first 201 before it gives you the real problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is what makes SMS bugs annoying.&lt;/p&gt;

&lt;p&gt;You call the Messages API. Twilio accepts it. You get a SID back. Your app marks the notification as "sent" and everybody moves on.&lt;/p&gt;

&lt;p&gt;Then the message never arrives.&lt;/p&gt;

&lt;p&gt;Sometimes the destination number is bad. Sometimes the carrier rejects it. Sometimes the message moves through &lt;code&gt;queued&lt;/code&gt; or &lt;code&gt;sent&lt;/code&gt; and then lands in &lt;code&gt;undelivered&lt;/code&gt;. The first API call still looked fine, so the debugging starts late.&lt;/p&gt;

&lt;p&gt;That is the part I think a lot of Twilio examples skip. The useful test is not just "can I &lt;code&gt;POST /Messages.json&lt;/code&gt;?" The useful test is "what does my app do after Twilio accepts the message but delivery goes sideways?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The narrow workflow that matters
&lt;/h2&gt;

&lt;p&gt;For local testing, the flow I care about is very small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;send the message&lt;/li&gt;
&lt;li&gt;fetch the message again by SID&lt;/li&gt;
&lt;li&gt;check what status your app sees next&lt;/li&gt;
&lt;li&gt;confirm your retry, alerting, or customer-facing state does not pretend the message was delivered&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In Twilio terms, that usually starts here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.twilio.com/2010-04-01/Accounts/AC.../Messages.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"To=+14155551234"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"From=+15017122661"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"Body=Your order has shipped"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TWILIO_ACCOUNT_SID&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$TWILIO_AUTH_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then immediately becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.twilio.com/2010-04-01/Accounts/AC.../Messages/&amp;lt;message_sid&amp;gt;.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TWILIO_ACCOUNT_SID&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$TWILIO_AUTH_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second read is where the truth starts showing up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where teams usually fool themselves
&lt;/h2&gt;

&lt;p&gt;The easy mistake is treating &lt;code&gt;201 Created&lt;/code&gt; as success for the whole job.&lt;/p&gt;

&lt;p&gt;It is only success for the first step: Twilio accepted your request and created a message resource. It is not proof that the carrier accepted it, not proof that the handset got it, and definitely not proof that your follow-up logic handled a bad delivery outcome correctly.&lt;/p&gt;

&lt;p&gt;That gap creates a bunch of quiet bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your UI says "message sent" too early&lt;/li&gt;
&lt;li&gt;your retry job never runs because the app thinks the work is already done&lt;/li&gt;
&lt;li&gt;your webhook or polling code handles &lt;code&gt;delivered&lt;/code&gt; but not &lt;code&gt;undelivered&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;support ends up debugging customer complaints that started as an async status transition, not a failed API request&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would test before production
&lt;/h2&gt;

&lt;p&gt;If I only had time for one narrow Twilio test, I would test the branch where the message gets accepted first and fails later.&lt;/p&gt;

&lt;p&gt;That means checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your app stores the SID from the first response&lt;/li&gt;
&lt;li&gt;you can fetch the message again and read the later status instead of assuming the first response told the whole story&lt;/li&gt;
&lt;li&gt;your callback or polling path updates the final state correctly&lt;/li&gt;
&lt;li&gt;the rest of your system reacts differently to &lt;code&gt;delivered&lt;/code&gt; vs &lt;code&gt;undelivered&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same reason SMS flows feel harder than they look in Postman. Postman proves the request shape. It does not prove the delivery story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a better local test than another happy path
&lt;/h2&gt;

&lt;p&gt;A happy path SMS demo is easy to trust too early.&lt;/p&gt;

&lt;p&gt;You send one message, get one SID, see one success response, and tell yourself the integration is basically done.&lt;/p&gt;

&lt;p&gt;But the production pain usually sits in the branch after that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;carrier rejection&lt;/li&gt;
&lt;li&gt;invalid destination&lt;/li&gt;
&lt;li&gt;quiet delivery failure&lt;/li&gt;
&lt;li&gt;app state that never gets corrected after the async update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I like narrow workflow tests more than broad "Twilio integration" tests. A small delivery-failure workflow tells you more than a generic sandbox smoke test ever will.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to make this easier locally
&lt;/h2&gt;

&lt;p&gt;I wrote a runnable &lt;a href="https://fetchsandbox.com/docs/twilio" rel="noopener noreferrer"&gt;Twilio docs portal on FetchSandbox&lt;/a&gt; because I kept wanting the same thing: send the message, fetch it again, and inspect the later state without jumping between too many tools.&lt;/p&gt;

&lt;p&gt;The useful part is not just mocking the first response. It is being able to test the full "accepted first, failed later" branch before production traffic teaches it to you.&lt;/p&gt;

&lt;p&gt;Curious how other people handle this one. Do you mostly poll message state, rely on callbacks, or just treat the first 201 as "good enough" until support says otherwise?&lt;/p&gt;

</description>
      <category>twilio</category>
      <category>sms</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Why Paddle's subscription.activated arrives before subscription.created</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Thu, 30 Apr 2026 23:57:15 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/why-paddles-subscriptionactivated-arrives-before-subscriptioncreated-4c5i</link>
      <guid>https://dev.to/fetchsandbox/why-paddles-subscriptionactivated-arrives-before-subscriptioncreated-4c5i</guid>
      <description>&lt;p&gt;&lt;em&gt;You'd think events fire in the order things happen. They don't.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I was building a Paddle integration last week. Subscription billing, nothing fancy. Customer clicks buy, Paddle handles checkout, my app gets webhooks and updates the database.&lt;/p&gt;

&lt;p&gt;The flow should be simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer completes checkout&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subscription.created&lt;/code&gt; fires&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subscription.activated&lt;/code&gt; fires&lt;/li&gt;
&lt;li&gt;My app inserts a row on &lt;code&gt;.created&lt;/code&gt;, updates status on &lt;code&gt;.activated&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's what I built. It worked great in my head.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;In production, &lt;code&gt;subscription.activated&lt;/code&gt; arrived &lt;em&gt;before&lt;/em&gt; &lt;code&gt;subscription.created&lt;/code&gt; about 30% of the time.&lt;/p&gt;

&lt;p&gt;My handler did a database insert on &lt;code&gt;subscription.created&lt;/code&gt;:&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;paddleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="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="s1"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an update on &lt;code&gt;subscription.activated&lt;/code&gt;:&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.activated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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="s1"&gt;active&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="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paddleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;.activated&lt;/code&gt; arrived first, the update found zero rows. No error, no exception. The &lt;code&gt;WHERE&lt;/code&gt; clause just matched nothing. The update silently did nothing.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;.created&lt;/code&gt; arrived and inserted the row with status &lt;code&gt;created&lt;/code&gt;. But the &lt;code&gt;.activated&lt;/code&gt; event was already gone. So the subscription was stuck in &lt;code&gt;created&lt;/code&gt; status forever.&lt;/p&gt;

&lt;p&gt;Customers had paid. Paddle showed them as active. My app showed them as pending. Support tickets started coming in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Paddle does not guarantee webhook delivery order. Their docs mention it briefly but it's easy to miss when you're focused on the API endpoints.&lt;/p&gt;

&lt;p&gt;The events are fired from different internal services. &lt;code&gt;subscription.created&lt;/code&gt; comes from the subscription service. &lt;code&gt;subscription.activated&lt;/code&gt; comes from the billing service after payment confirmation. They are async. They race.&lt;/p&gt;

&lt;p&gt;This is not unique to Paddle either. Stripe has the same problem with &lt;code&gt;payment_intent.created&lt;/code&gt; vs &lt;code&gt;charge.succeeded&lt;/code&gt;. Most payment providers have some version of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The handler needs to be idempotent and order-independent. Every event should be able to create or update:&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="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.activated&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription.activated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; 
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;paddleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onConflictDoUpdate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paddleId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&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="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`CASE WHEN &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; = 'active' THEN 'active' ELSE &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; END`&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both events can create the row if it doesn't exist&lt;/li&gt;
&lt;li&gt;On conflict, &lt;code&gt;active&lt;/code&gt; always wins over &lt;code&gt;created&lt;/code&gt; regardless of arrival order&lt;/li&gt;
&lt;li&gt;No silent failures, no missing updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing this is the real problem
&lt;/h2&gt;

&lt;p&gt;The ordering bug is easy to fix once you know about it. The hard part is reproducing it during development.&lt;/p&gt;

&lt;p&gt;You can't control the order Paddle sends webhooks. You can't make &lt;code&gt;.activated&lt;/code&gt; arrive first on demand. In testing you might run through the flow 20 times and the events always arrive in order. Then in production with real network latency and load, they don't.&lt;/p&gt;

&lt;p&gt;I ended up testing this by sending the webhook events manually in the wrong order against a local sandbox. &lt;code&gt;activated&lt;/code&gt; first, then &lt;code&gt;created&lt;/code&gt;. Immediately saw the bug. Fixed it in 10 minutes.&lt;/p&gt;

&lt;p&gt;The debugging in production took 4 hours.&lt;/p&gt;

&lt;p&gt;If you're integrating Paddle or any payment provider with webhooks, test with events arriving in every possible order. Not just the happy path order from the docs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/paddle?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=paddle-activated-before-created" rel="noopener noreferrer"&gt;Test Paddle webhook ordering in a sandbox →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Webhook events are not a queue. They are concurrent messages from different services that happen to be about the same thing. Your handler has to treat every event as potentially the first one it sees for that resource.&lt;/p&gt;

&lt;p&gt;If your handler has an insert for one event type and an update for another, you have this bug. You just haven't hit it in production yet.&lt;/p&gt;

</description>
      <category>paddle</category>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
