<?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>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>
    <item>
      <title>How to test Stripe Connect transfers without a connected account</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Thu, 30 Apr 2026 13:20:11 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/how-to-test-stripe-connect-transfers-without-a-connected-account-55m3</link>
      <guid>https://dev.to/fetchsandbox/how-to-test-stripe-connect-transfers-without-a-connected-account-55m3</guid>
      <description>&lt;p&gt;&lt;em&gt;The transfer API call is easy. The testing setup is not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Stripe Connect lets platforms move money to connected accounts.&lt;/p&gt;

&lt;p&gt;The API is four calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a transfer to a connected account&lt;/li&gt;
&lt;li&gt;Verify the transfer.created webhook fires&lt;/li&gt;
&lt;li&gt;Create a payout from the connected account&lt;/li&gt;
&lt;li&gt;Verify the payout.created webhook fires&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On paper, that is maybe ten minutes of reading.&lt;/p&gt;

&lt;p&gt;In practice, getting to the point where you can actually test this flow takes much longer than the code itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup tax
&lt;/h2&gt;

&lt;p&gt;Before you can make a single test transfer, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A platform account with Connect enabled&lt;/li&gt;
&lt;li&gt;A connected account that has completed onboarding&lt;/li&gt;
&lt;li&gt;That connected account needs a verified external account (bank details) for payouts&lt;/li&gt;
&lt;li&gt;Your platform needs the right capabilities configured&lt;/li&gt;
&lt;li&gt;Enough test balance in the right currency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a lot of infrastructure for what is ultimately four API calls.&lt;/p&gt;

&lt;p&gt;Most developers skip the full setup. They test the transfer call in isolation, hardcode a connected account ID, and hope the payout works the same way in production.&lt;/p&gt;

&lt;p&gt;That is not really testing the integration. That is testing one endpoint with a hardcoded parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people skip
&lt;/h2&gt;

&lt;p&gt;The webhook verification is the part that matters most and gets tested least.&lt;/p&gt;

&lt;p&gt;In production, &lt;code&gt;transfer.created&lt;/code&gt; is how your system knows funds actually moved. If your webhook handler is not processing that event correctly, your app might show a successful transfer to the user while the money is still sitting in your platform account.&lt;/p&gt;

&lt;p&gt;Same for &lt;code&gt;payout.created&lt;/code&gt;. Your connected account's dashboard should reflect the payout, but if the webhook never fires or your handler drops it, the seller thinks they got paid and you have a support ticket in 48 hours.&lt;/p&gt;

&lt;p&gt;Testing this against real Stripe means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up a webhook endpoint reachable from the internet&lt;/li&gt;
&lt;li&gt;Waiting for Stripe to deliver the event&lt;/li&gt;
&lt;li&gt;Parsing the event and verifying the payload contains the right transfer ID&lt;/li&gt;
&lt;li&gt;Making sure your handler is idempotent in case Stripe retries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams punt on this and test it manually in staging. Once. And then never again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the flow actually looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /v1/transfers
  → 200: { id: "tr_1abc...", amount: 5000, currency: "usd" }
  → webhook: transfer.created fires with transfer ID

GET /v1/transfers/tr_1abc...
  → 200: { status: "paid" }

POST /v1/payouts
  → 200: { id: "po_2def...", amount: 5000 }
  → webhook: payout.created fires with payout ID

GET /v1/payouts/po_2def...
  → 200: { status: "paid" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four calls. Two webhooks. Each step depends on IDs and state from the previous one.&lt;/p&gt;

&lt;p&gt;The transfer ID from step 1 is what your system uses to track the money. The payout in step 3 only makes sense if the transfer actually landed. The webhooks in between are what your production system will rely on to update its own state.&lt;/p&gt;

&lt;p&gt;If you only test each endpoint individually, you are testing four unrelated API calls. You are not testing a transfer-to-payout flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between endpoint and flow
&lt;/h2&gt;

&lt;p&gt;This is the same pattern that shows up across almost every multi-step API integration.&lt;/p&gt;

&lt;p&gt;Each endpoint works fine on its own. The docs explain each one clearly. But the docs do not usually explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What order to call them in&lt;/li&gt;
&lt;li&gt;Which IDs carry from one step to the next&lt;/li&gt;
&lt;li&gt;Which webhooks fire between steps&lt;/li&gt;
&lt;li&gt;What happens if you skip a step or call them out of order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap is where the debugging time lives.&lt;/p&gt;

&lt;p&gt;Not in the API call itself. In the space between calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the whole thing without the setup
&lt;/h2&gt;

&lt;p&gt;I wanted a way to test this flow without setting up a connected account, without configuring capabilities, and without waiting for webhook delivery.&lt;/p&gt;

&lt;p&gt;So I built a sandbox where you paste the curl and the state persists between calls. The transfer creates a real resource in the sandbox. The webhook fires automatically. The payout references the actual transfer. The IDs chain through every step.&lt;/p&gt;

&lt;p&gt;No Stripe API key. No connected account. No webhook endpoint to host.&lt;/p&gt;

&lt;p&gt;You verify the workflow, not just individual endpoints.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/stripe?page=workflow-connect-transfer-payout" rel="noopener noreferrer"&gt;Try the Stripe Connect workflow&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest version
&lt;/h2&gt;

&lt;p&gt;I do not think this replaces Stripe test mode. If you are testing real card capture, real checkout flows, or anything that touches Stripe's actual infrastructure, you should use test mode.&lt;/p&gt;

&lt;p&gt;But if you are trying to answer a simpler question — "does my transfer-to-payout flow work end to end, including the webhooks?" — you should not need 30 minutes of setup to find out.&lt;/p&gt;

&lt;p&gt;That is the part I am trying to fix with &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Curious how other teams handle Connect testing. Do you maintain a permanent test connected account, set up a fresh one for each project, or just skip the webhook verification and hope for the best?&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Test Paddle webhooks without account verification</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Wed, 29 Apr 2026 14:07:21 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-paddle-webhooks-without-account-verification-dk4</link>
      <guid>https://dev.to/fetchsandbox/test-paddle-webhooks-without-account-verification-dk4</guid>
      <description>&lt;p&gt;&lt;em&gt;You can't test a Paddle subscription cancel webhook until Paddle has verified your business. That takes days.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Paddle's billing API handles subscriptions, payments, and the entire checkout flow. The API is clean and the docs are solid.&lt;/p&gt;

&lt;p&gt;The problem is the sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification before testing
&lt;/h2&gt;

&lt;p&gt;To get a Paddle sandbox account, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign up and create a separate sandbox login&lt;/li&gt;
&lt;li&gt;Wait for Paddle to verify your business (this can take days)&lt;/li&gt;
&lt;li&gt;Set up products and prices in the sandbox dashboard&lt;/li&gt;
&lt;li&gt;Configure webhook URLs&lt;/li&gt;
&lt;li&gt;Use test card numbers for checkout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this before you can test whether your code correctly handles a &lt;code&gt;subscription.canceled&lt;/code&gt; webhook.&lt;/p&gt;

&lt;p&gt;If you're building a billing integration and you want to test the full lifecycle — create subscription, pause, resume, cancel — you're waiting on Paddle's verification process before you can write a single test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifecycle is the hard part
&lt;/h2&gt;

&lt;p&gt;The individual API calls are straightforward. The hard part is the lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a subscription → status is &lt;code&gt;active&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pause it → status changes to &lt;code&gt;paused&lt;/code&gt;, webhook fires&lt;/li&gt;
&lt;li&gt;Resume it → status changes back to &lt;code&gt;active&lt;/code&gt;, webhook fires&lt;/li&gt;
&lt;li&gt;Cancel it → status changes to &lt;code&gt;canceled&lt;/code&gt;, webhook fires&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each transition produces a webhook event. If your handler doesn't process them in the right order, or misses one, your app's subscription state drifts from Paddle's.&lt;/p&gt;

&lt;p&gt;You can't test this sequence without a working sandbox. And you can't get a working sandbox without verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the lifecycle without waiting
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/paddle" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; has a Paddle sandbox with the full subscription lifecycle. No verification, no account setup, no test cards.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST /subscriptions&lt;/code&gt; — create a subscription&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /subscriptions/{id}/pause&lt;/code&gt; — pause it, webhook fires&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /subscriptions/{id}/resume&lt;/code&gt; — resume it, webhook fires&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /subscriptions/{id}/cancel&lt;/code&gt; — cancel it, webhook fires&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step is stateful. The subscription ID chains between requests. The status transitions follow Paddle's actual state machine. Webhooks fire at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for billing
&lt;/h2&gt;

&lt;p&gt;Billing bugs are silent. A missed &lt;code&gt;subscription.canceled&lt;/code&gt; webhook doesn't throw an error — it just means a cancelled customer keeps access. You find out weeks later when you're reconciling revenue and the numbers don't match.&lt;/p&gt;

&lt;p&gt;Testing the full lifecycle — not just the happy path, but pause/resume/cancel — is how you catch these before they cost you money.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/paddle" rel="noopener noreferrer"&gt;Try the Paddle sandbox&lt;/a&gt; — no signup, no verification, runs in your browser.&lt;/p&gt;

</description>
      <category>paddle</category>
      <category>webhooks</category>
      <category>api</category>
      <category>testing</category>
    </item>
    <item>
      <title>Test Datadog API endpoints without an API key</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Mon, 27 Apr 2026 13:56:55 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/test-datadog-api-endpoints-without-an-api-key-2b9m</link>
      <guid>https://dev.to/fetchsandbox/test-datadog-api-endpoints-without-an-api-key-2b9m</guid>
      <description>&lt;p&gt;&lt;em&gt;You need an API key and an application key before you can make a single call. That's two keys, two permissions scopes, and a paid account.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Datadog's API is well-documented. The endpoints are clean, the response shapes are consistent, and the SDKs cover every major language.&lt;/p&gt;

&lt;p&gt;The problem is testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two keys before you start
&lt;/h2&gt;

&lt;p&gt;To call any Datadog endpoint, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;DD-API-KEY&lt;/code&gt; — tied to your organization&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;DD-APPLICATION-KEY&lt;/code&gt; — tied to a specific user with specific permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't get either without a Datadog account. Free trials exist, but they require a credit card and install an agent on your infrastructure.&lt;/p&gt;

&lt;p&gt;If you're building an integration — say, a dashboard that pulls monitors and incidents, or a CI pipeline that posts deployment events — you have to set all of that up before you can test whether your code handles the response correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape matters more than the data
&lt;/h2&gt;

&lt;p&gt;Most of the time, what you actually need to validate isn't "does Datadog return my real monitors." It's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does my code handle the pagination format?&lt;/li&gt;
&lt;li&gt;What does the response look like when a monitor has no tags?&lt;/li&gt;
&lt;li&gt;What happens when I create an incident and immediately query it — does the state transition work?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are integration logic questions, not data questions. You don't need your production monitors to answer them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the flow without an account
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/datadog-v2" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; has a Datadog v2 sandbox with every endpoint callable immediately. No keys, no agent install, no account.&lt;/p&gt;

&lt;p&gt;The sandbox is stateful — create a monitor, then list monitors, and the one you just created shows up. Create an incident, transition it to resolved, and the state change persists.&lt;/p&gt;

&lt;p&gt;A workflow that tests the incident lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v2/incidents&lt;/code&gt; — create an incident&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/incidents/{id}&lt;/code&gt; — verify it exists with status &lt;code&gt;active&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PATCH /api/v2/incidents/{id}&lt;/code&gt; — transition to &lt;code&gt;resolved&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/v2/incidents/{id}&lt;/code&gt; — confirm the state changed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step chains IDs from the previous one. The sandbox handles this automatically.&lt;/p&gt;

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

&lt;p&gt;If you're building Datadog integrations and your test suite currently requires a live Datadog org, every test run is a network call to production. When Datadog has an outage or rate-limits your key, your CI breaks for reasons that have nothing to do with your code.&lt;/p&gt;

&lt;p&gt;Running the same tests against a sandbox that behaves like the real API — but doesn't require auth or network — isolates the thing you're actually testing: your integration logic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/datadog-v2" rel="noopener noreferrer"&gt;Try the Datadog sandbox&lt;/a&gt; — no signup, runs in your browser.&lt;/p&gt;

</description>
      <category>datadog</category>
      <category>api</category>
      <category>testing</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Testing Resend's template email flow end to end</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Sat, 25 Apr 2026 14:06:02 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/testing-resends-template-email-flow-end-to-end-44k3</link>
      <guid>https://dev.to/fetchsandbox/testing-resends-template-email-flow-end-to-end-44k3</guid>
      <description>&lt;p&gt;&lt;em&gt;Three API calls. One template ID that has to carry through all of them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Resend's API for sending a templated email is three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a template&lt;/li&gt;
&lt;li&gt;Publish the template&lt;/li&gt;
&lt;li&gt;Send an email using that template&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step depends on the one before it.&lt;/p&gt;

&lt;p&gt;The template ID from step 1 has to be passed into step 2. The published status from step 2 has to be confirmed before step 3 will actually send. And if you want to verify your integration end to end, you need the &lt;code&gt;template.created&lt;/code&gt; webhook to fire after step 1.&lt;/p&gt;

&lt;p&gt;That is a simple flow. But most developers do not actually test it as a flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How most people test this
&lt;/h2&gt;

&lt;p&gt;The typical approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a template in the Resend dashboard manually&lt;/li&gt;
&lt;li&gt;Copy the template ID into your code&lt;/li&gt;
&lt;li&gt;Call the send endpoint with that hardcoded ID&lt;/li&gt;
&lt;li&gt;Check your inbox&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That tests one thing: can your code call POST /emails with a valid template ID?&lt;/p&gt;

&lt;p&gt;It does not test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether your code correctly creates templates via the API&lt;/li&gt;
&lt;li&gt;Whether the publish step returns the right status&lt;/li&gt;
&lt;li&gt;Whether the template ID chains correctly from creation to sending&lt;/li&gt;
&lt;li&gt;Whether the template.created webhook fires and your handler processes it&lt;/li&gt;
&lt;li&gt;Whether the whole flow works when run programmatically, not manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hardcoding a template ID is not integration testing. It is calling one endpoint with a known parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the real flow looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /templates
  → 200: { id: "tpl_abc123", object: "template" }
  → webhook: template.created fires

POST /templates/tpl_abc123/publish
  → 200: { id: "tpl_abc123", status: "published" }
  → webhook: template.published fires

POST /emails
  → 200: { id: "email_xyz789" }
  body: { template_id: "tpl_abc123", to: "user@example.com" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three calls. Two webhooks. The template ID threads through every step.&lt;/p&gt;

&lt;p&gt;The question you should be able to answer before shipping is not "does POST /emails work?" It is "does my entire template-to-send pipeline work when the template is created, published, and referenced programmatically?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The webhook part
&lt;/h2&gt;

&lt;p&gt;Most email integrations skip webhook verification entirely.&lt;/p&gt;

&lt;p&gt;That is understandable. For a simple "send one email" integration, you might not need webhooks at all.&lt;/p&gt;

&lt;p&gt;But once you are building a system that creates templates dynamically — onboarding flows, transactional sequences, marketing automation — you probably want to know when a template was created and when it was published.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;template.created&lt;/code&gt; tells your system the template exists. &lt;code&gt;template.published&lt;/code&gt; tells your system it is safe to reference in send calls.&lt;/p&gt;

&lt;p&gt;If your code sends an email with a template that has not finished publishing, you get a confusing error that looks like a bad template ID. The actual issue is a timing problem in your flow.&lt;/p&gt;

&lt;p&gt;Testing the webhooks is how you catch this before production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The testing setup problem
&lt;/h2&gt;

&lt;p&gt;To test this flow against real Resend, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Resend API key&lt;/li&gt;
&lt;li&gt;A verified sender domain&lt;/li&gt;
&lt;li&gt;A real email address to receive the test send&lt;/li&gt;
&lt;li&gt;A webhook endpoint reachable from the internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is reasonable for a production integration test.&lt;/p&gt;

&lt;p&gt;It is less reasonable for the Tuesday afternoon when you just want to know if your template creation flow works before you wire it into your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it without any of that
&lt;/h2&gt;

&lt;p&gt;I built a sandbox where you can run this exact flow without a Resend API key, without a verified domain, and without hosting a webhook endpoint.&lt;/p&gt;

&lt;p&gt;You call POST /templates. The sandbox creates a template and returns a real ID. You call POST /templates/{id}/publish. The status updates. You call POST /emails with that template ID. The send succeeds. The webhooks fire automatically between steps.&lt;/p&gt;

&lt;p&gt;The state persists. The IDs chain. The whole flow runs in about 30 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fetchsandbox.com/docs/resend?page=workflow-template_email_workflow" rel="noopener noreferrer"&gt;Try the Resend template workflow&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for email integrations specifically
&lt;/h2&gt;

&lt;p&gt;Email is one of those integrations where "it worked in testing" and "it works in production" can be very different things.&lt;/p&gt;

&lt;p&gt;Templates get created but not published. Send calls reference stale template IDs. Webhook handlers assume the template exists before the creation event arrives.&lt;/p&gt;

&lt;p&gt;These are not exotic edge cases. They are the normal bugs that happen when you test endpoints in isolation instead of testing the flow.&lt;/p&gt;

&lt;p&gt;The fix is not complicated. Run the whole chain once, in order, with state that carries between steps. If the template ID from step 1 appears correctly in step 3, your integration probably works. If it does not, you want to find out now, not when your onboarding emails start failing.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://fetchsandbox.com" rel="noopener noreferrer"&gt;FetchSandbox&lt;/a&gt; does. Turns any OpenAPI spec into a stateful sandbox with workflows.&lt;/p&gt;

&lt;p&gt;Curious how other people handle email integration testing. Do you test against real Resend with test keys, use a mock, or mostly just trust the dashboard and check your inbox?&lt;/p&gt;

</description>
      <category>email</category>
      <category>api</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to test GitHub merge conflicts locally</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Wed, 22 Apr 2026 04:47:25 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/how-to-test-github-merge-conflicts-locally-35d0</link>
      <guid>https://dev.to/fetchsandbox/how-to-test-github-merge-conflicts-locally-35d0</guid>
      <description>&lt;p&gt;&lt;em&gt;The happy path for a pull request is boring. The merge conflict is where the integration usually gets real.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most GitHub API demos stop too early.&lt;/p&gt;

&lt;p&gt;They show how to open the pull request, fetch it, maybe list reviews, and then call it done.&lt;/p&gt;

&lt;p&gt;That is useful for proving your auth works. It is not enough for proving your PR automation works.&lt;/p&gt;

&lt;p&gt;The harder branch is the one where the pull request exists, the metadata looks fine, and then the merge step hits a conflict. That is the path that decides whether your bot, internal tool, or release workflow can recover sanely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small workflow that actually matters
&lt;/h2&gt;

&lt;p&gt;For this kind of test, I care about a very small flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;open the pull request&lt;/li&gt;
&lt;li&gt;fetch the PR again to inspect its status&lt;/li&gt;
&lt;li&gt;try the merge step&lt;/li&gt;
&lt;li&gt;see what your system does when the PR is not cleanly mergeable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That starts with something like:&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.github.com/repos/acme-corp/api-gateway/pulls"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "title": "fix: handle nil pointer in rate limiter",
    "body": "Fixes #142. Adds nil check before accessing connection pool.",
    "head": "fix/rate-limiter-nil",
    "base": "main"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then quickly 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.github.com/repos/acme-corp/api-gateway/pulls/&amp;lt;number&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$GITHUB_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github+json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is not just that the PR exists. It is what your code does when the branch cannot merge cleanly after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where teams usually get fooled
&lt;/h2&gt;

&lt;p&gt;The trap is treating "pull request created" as proof that the automation is basically done.&lt;/p&gt;

&lt;p&gt;But a lot of GitHub workflows only become interesting after creation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the branch is stale&lt;/li&gt;
&lt;li&gt;another change landed on &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the PR looks valid but is not mergeable&lt;/li&gt;
&lt;li&gt;your bot tries to continue anyway&lt;/li&gt;
&lt;li&gt;your UI says "ready" even though a human now has to intervene&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the same pattern that shows up in a lot of async integrations: step 1 succeeds, so the app assumes the whole job is healthy.&lt;/p&gt;

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

&lt;p&gt;If I only had time for one narrow GitHub API test, I would test the merge-conflict branch.&lt;/p&gt;

&lt;p&gt;I would check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can my app detect that the PR exists but is not safely mergeable?&lt;/li&gt;
&lt;li&gt;do I expose the right state to the user, instead of pretending it is still a normal merge path?&lt;/li&gt;
&lt;li&gt;do retries keep hammering the same PR, or do we stop and surface the conflict clearly?&lt;/li&gt;
&lt;li&gt;if there is a webhook or follow-up read in the flow, does the final state stay honest?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part matters more than the first API call.&lt;/p&gt;

&lt;p&gt;A lot of automation bugs are not "could not create PR."&lt;/p&gt;

&lt;p&gt;They are more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"we created it, then misread the later state"&lt;/li&gt;
&lt;li&gt;"we retried the wrong step"&lt;/li&gt;
&lt;li&gt;"we told the user merge was blocked too late"&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A happy-path pull request test mostly proves you can talk to GitHub.&lt;/p&gt;

&lt;p&gt;A merge-conflict test tells you whether your integration can survive a normal repo state that changes under it.&lt;/p&gt;

&lt;p&gt;That is a much better signal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;release tooling&lt;/li&gt;
&lt;li&gt;AI coding agents opening PRs&lt;/li&gt;
&lt;li&gt;internal automation that tries to auto-merge fixes&lt;/li&gt;
&lt;li&gt;any workflow that assumes the branch state will stay clean between creation and merge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also more realistic. Real repositories drift constantly. If your testing never touches the conflict branch, your merge logic is probably getting too much credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I think is underrated
&lt;/h2&gt;

&lt;p&gt;The useful question is not just:&lt;/p&gt;

&lt;p&gt;"can I create a PR through the GitHub API?"&lt;/p&gt;

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

&lt;p&gt;"when the PR stops being mergeable, do I still know what happened and what to do next?"&lt;/p&gt;

&lt;p&gt;That is the gap between endpoint testing and workflow testing.&lt;/p&gt;

&lt;p&gt;The endpoint docs explain the request shape. The real integration job is deciding what your system should do after the branch state changes.&lt;/p&gt;

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

&lt;p&gt;I like testing this as a narrow workflow instead of a broad "GitHub API integration" check.&lt;/p&gt;

&lt;p&gt;That is also why I keep a runnable &lt;a href="https://fetchsandbox.com/docs/github" rel="noopener noreferrer"&gt;GitHub docs portal on FetchSandbox&lt;/a&gt; around. The useful part is being able to open the PR, fetch it again, and inspect the later branch in one place instead of stopping at creation.&lt;/p&gt;

&lt;p&gt;For me, the merge-conflict path is one of those cases where a small failure-mode test teaches more than a longer happy-path demo.&lt;/p&gt;

&lt;p&gt;Curious how other people handle this. If your GitHub automation opens a PR and later hits a conflict, do you surface that as a first-class state, or does it still turn into log-diving and confused retries?&lt;/p&gt;

</description>
      <category>github</category>
      <category>api</category>
      <category>testing</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How to test Stripe webhook signatures locally without breaking verification</title>
      <dc:creator>FetchSandbox</dc:creator>
      <pubDate>Fri, 17 Apr 2026 16:24:17 +0000</pubDate>
      <link>https://dev.to/fetchsandbox/how-to-test-stripe-webhook-signatures-locally-without-breaking-verification-1236</link>
      <guid>https://dev.to/fetchsandbox/how-to-test-stripe-webhook-signatures-locally-without-breaking-verification-1236</guid>
      <description>&lt;p&gt;&lt;em&gt;The request usually succeeds first. The signature check is where the time disappears.&lt;/em&gt;&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/stripe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webhookSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you usually need the raw body on that route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/stripe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webhookSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard part is not just fixing the route once. It is trusting that the whole flow still works after the fix:&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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