<?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: CJ Avilla</title>
    <description>The latest articles on DEV Community by CJ Avilla (@cjav_dev).</description>
    <link>https://dev.to/cjav_dev</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%2F346915%2F5852db7c-85be-41eb-b1a6-636940411821.png</url>
      <title>DEV Community: CJ Avilla</title>
      <link>https://dev.to/cjav_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cjav_dev"/>
    <language>en</language>
    <item>
      <title>Introducing the (experimental!) Stainless SQL SDK generator</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Wed, 18 Feb 2026 15:09:31 +0000</pubDate>
      <link>https://dev.to/stainlessapi/introducing-the-experimental-stainless-sql-sdk-generator-2bin</link>
      <guid>https://dev.to/stainlessapi/introducing-the-experimental-stainless-sql-sdk-generator-2bin</guid>
      <description>&lt;p&gt;If you run analytics, reconciliation, or batch jobs in PostgreSQL, pulling data from REST APIs usually means ETL scripts, intermediary services, or brittle sync pipelines. The data you need is one HTTP request away, but PostgreSQL is not designed to call HTTP endpoints as part of a query plan.&lt;/p&gt;

&lt;p&gt;You can bolt on ad-hoc HTTP functions, but you lose typing, pagination ergonomics, and a spec-driven interface.&lt;/p&gt;

&lt;p&gt;We built an experimental SQL SDK generator that turns an OpenAPI spec into a &lt;a href="https://www.postgresql.org/docs/current/sql-createextension.html" rel="noopener noreferrer"&gt;PostgreSQL extension&lt;/a&gt; that uses &lt;a href="https://www.postgresql.org/docs/current/plpython.html" rel="noopener noreferrer"&gt;PL/Python&lt;/a&gt; for HTTP requests. Every API endpoint becomes a SQL function. Every response type becomes a &lt;a href="https://www.postgresql.org/docs/current/rowtypes.html" rel="noopener noreferrer"&gt;composite type&lt;/a&gt;. (We explain why we chose SQL functions over Foreign Data Wrappers below.)&lt;/p&gt;

&lt;p&gt;A SQL SDK enables users to query your API using standard SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get: REST API calls as SQL functions
&lt;/h2&gt;

&lt;p&gt;To show you how our SQL SDK generator works, we used a subset of the &lt;a href="https://stripe.com/" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt; OpenAPI spec to generate &lt;a href="https://github.com/stainless-commons/stripe-sql" rel="noopener noreferrer"&gt;an experimental SQL SDK&lt;/a&gt;. You can install it by cloning &lt;a href="https://github.com/stainless-commons/stripe-sql" rel="noopener noreferrer"&gt;the extension repo&lt;/a&gt; and running &lt;code&gt;./scripts/repl&lt;/code&gt; or by manually building with &lt;code&gt;make install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jhj5uise89t9mbrjz5g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jhj5uise89t9mbrjz5g.gif" alt="Demo showing how to query with SQL DSK functions" width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each API endpoint maps to a SQL function. Each resource maps to a PostgreSQL schema. The result is a typed interface to a REST API, accessible from any PostgreSQL client.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The extension is built on two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PL/Python functions&lt;/strong&gt; call a Stainless-generated Python SDK to handle HTTP requests, authentication, retries, request serialization, response parsing, and other networking concerns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL function wrappers&lt;/strong&gt; initialize the client, perform lenient type adaptation from Python objects to PostgreSQL composite types, and expose a typed SQL interface. A single client is initialized per session for performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paginated endpoints employ SQL functions that use &lt;code&gt;WITH RECURSIVE&lt;/code&gt; CTEs to repeatedly fetch pages using PL/Python function calls. Because the outer SQL function can be used &lt;a href="https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions" rel="noopener noreferrer"&gt;inline&lt;/a&gt;, PostgreSQL can push down &lt;code&gt;LIMIT&lt;/code&gt; clauses and stop requesting new pages once enough rows have been produced.&lt;/p&gt;

&lt;p&gt;If pagination were implemented entirely in Python, PostgreSQL would buffer the full result set at the PL/Python boundary before applying &lt;code&gt;LIMIT&lt;/code&gt;. By keeping auto-pagination in SQL, the query planner retains control over row consumption and can short-circuit additional HTTP requests.&lt;/p&gt;

&lt;p&gt;The result is a standard PostgreSQL extension. Install with &lt;code&gt;make install&lt;/code&gt; and load with &lt;code&gt;CREATE EXTENSION&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;plpython3u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure with &lt;code&gt;ALTER DATABASE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, to configure an API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- For persistent configuration (ensure your logs are secured):&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;my_database&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sk_test_...'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Or for a single session:&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sk_test_...'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1cnbaaomf9t6nuk2hdt1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1cnbaaomf9t6nuk2hdt1.jpeg" alt="Two layers of the Stainless SQL SDK from the SQL interface down into the HTTP layer and API response back and then back with typed composite rows" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Type-safe composite types, not &lt;code&gt;JSONB&lt;/code&gt; blobs
&lt;/h2&gt;

&lt;p&gt;Each API resource maps to a PostgreSQL schema. &lt;code&gt;stripe_customer&lt;/code&gt; contains the &lt;code&gt;customer&lt;/code&gt; composite type with typed fields: &lt;code&gt;id TEXT&lt;/code&gt;, &lt;code&gt;created BIGINT&lt;/code&gt;, &lt;code&gt;email TEXT&lt;/code&gt;, &lt;code&gt;livemode BOOLEAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The generator types fields where the spec is precise. Nested objects map to composite types, and arrays become typed arrays.&lt;/p&gt;

&lt;p&gt;You get real column access instead of &lt;code&gt;JSONB-&amp;gt;&amp;gt;'field'&lt;/code&gt; casting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Typed fields, not JSONB extraction&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;livemode&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For complex parameters, the extension generates &lt;code&gt;make_*&lt;/code&gt; constructor functions. The generator produces these from the OpenAPI spec, and they stay in sync when the spec changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Create a customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'jane@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Jane Doe'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List customers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension handles pagination automatically. A &lt;code&gt;LIMIT 200&lt;/code&gt; for an API that returns 100 items per page makes exactly two HTTP requests. Apply &lt;code&gt;LIMIT&lt;/code&gt; as close to the API function call as possible so PostgreSQL can stop consuming rows once the limit is satisfied.&lt;/p&gt;

&lt;p&gt;The real payoff is combining API data with your existing tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stripe_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query joins live Stripe customer data from the API with your local &lt;code&gt;orders&lt;/code&gt; table. This replaces an ETL pipeline and staging tables with a single query.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0p98vp952s5snve4g8b6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0p98vp952s5snve4g8b6.png" alt="Stainless SQL SDK generator vs traditional" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where SQL SDKs fit
&lt;/h2&gt;

&lt;p&gt;SQL SDKs work well in many workflows, but they’re not a universal solution for every problem. They are most effective when you want to pull API data into analytical queries, scheduled jobs, or batch pipelines that already run inside PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  ETL and data sync
&lt;/h3&gt;

&lt;p&gt;Combine materialized views with &lt;code&gt;pg_cron&lt;/code&gt; for scheduled data pulls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;stripe_customers&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stripe_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;        
  &lt;span class="n"&gt;job_name&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'refresh-stripe-customers'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;schedule&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'0 */4 * * *'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;command&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'REFRESH MATERIALIZED VIEW CONCURRENTLY stripe_customers'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every four hours, your database has a fresh snapshot of Stripe customers. Query it like any other table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Business analytics
&lt;/h3&gt;

&lt;p&gt;Ad-hoc queries that combine API data with your operational database: revenue reconciliation, customer segmentation, churn analysis. The queries run where the data already lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch LLM workflows
&lt;/h3&gt;

&lt;p&gt;Select the latest support tickets, call an LLM classification endpoint, and write labels back to a table for downstream reporting. As LLM APIs publish OpenAPI specs, SQL SDKs make batch inference from SQL practical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where SQL SDKs are not a fit
&lt;/h3&gt;

&lt;p&gt;SQL SDKs are not intended for low latency production OLTP paths. Each function call issues one or more HTTP requests, introducing network latency, rate limits, and remote failure modes. PostgreSQL’s planner optimizes relational operators over local data; it is not a cost based planner for REST APIs. Core SQL concepts such as predicate pushdown, join reordering, and index selectivity do not map cleanly to HTTP endpoints, so small query changes can result in additional remote calls. Use SQL SDKs for analytical and batch workloads where expressiveness and integration matter more than tail latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not generate Foreign Data Wrappers?
&lt;/h2&gt;

&lt;p&gt;Generating &lt;a href="https://wiki.postgresql.org/wiki/Foreign_data_wrappers" rel="noopener noreferrer"&gt;Foreign Data Wrappers&lt;/a&gt; (FDWs) for APIs is appealing, but there are several drawbacks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performance is hard to reason about&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A REST API FDW would hide the HTTP requests it makes.&lt;/p&gt;

&lt;p&gt;The FDW has to determine which requests to make based on a query, making it a query planner for HTTP requests. A small change to a query could silently result in a less performant plan with many more HTTP requests. This would not be easy to debug.&lt;/p&gt;

&lt;p&gt;An FDW over a REST API ends up being a leaky abstraction.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;APIs are not always relational&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FDWs shine when the data they’re modeling is relational. If the data an API provides is not relational, then it’s not well-suited for being represented as tables in an FDW.&lt;/p&gt;

&lt;p&gt;Plain SQL functions work for any API.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A new interface to learn&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modeling API data in FDW tables would result in a different interface from the API and its SDKs, which means your users would have to learn a new API.&lt;/p&gt;

&lt;p&gt;Plain SQL functions grouped by resource matches the existing structure of your API and its SDKs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current status: experimental
&lt;/h2&gt;

&lt;p&gt;The SQL SDK generator works best with OpenAPI 3.x specifications that use standard request and response bodies and predictable pagination patterns. We generated a Stripe SQL SDK as the first example, but the generator is not Stripe-specific.&lt;/p&gt;

&lt;p&gt;This is an experimental release. We're sharing it because we want feedback from developers who work at the intersection of APIs and databases. We're also expanding coverage and want to hear about OpenAPI specs that break the generator.&lt;/p&gt;

&lt;p&gt;Expect breaking changes. This is not ready for production workloads. We are actively developing the generator and want to hear which use cases matter most to you.&lt;/p&gt;

&lt;p&gt;If you’re using our SQL SDK generator in any capacity we’d love to hear from you. &lt;a href="mailto:support@stainless.com"&gt;Let us know&lt;/a&gt; what you’re working on, if you run into any issues, or if you have feedback.&lt;/p&gt;

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

&lt;p&gt;The Stripe SQL SDK is available as a reference for what the generator produces. Clone the &lt;a href="https://github.com/stainless-commons/stripe-sql" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, run &lt;code&gt;make install&lt;/code&gt;, load with &lt;code&gt;CREATE EXTENSION stripe&lt;/code&gt;, and set &lt;code&gt;stripe.secret_key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Want a generated extension for your API? &lt;a href="https://qm8il.share.hsforms.com/2Vcw2JNtBToOZeR2CJsc40g" rel="noopener noreferrer"&gt;Sign up here&lt;/a&gt; to get early access and tell us which APIs you want to query from SQL. We are working toward supporting any API with an OpenAPI spec.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>webdev</category>
      <category>openai</category>
      <category>database</category>
    </item>
    <item>
      <title>Sharper than ever: the Stainless C# SDK generator is now generally available</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Tue, 17 Feb 2026 16:48:00 +0000</pubDate>
      <link>https://dev.to/stainlessapi/sharper-than-ever-the-stainless-c-sdk-generator-is-now-generally-available-19kl</link>
      <guid>https://dev.to/stainlessapi/sharper-than-ever-the-stainless-c-sdk-generator-is-now-generally-available-19kl</guid>
      <description>&lt;p&gt;C# has a unique place in the programming language ecosystem. It powers enterprise software, AAA games, and even front-end web development through &lt;a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor" rel="noopener noreferrer"&gt;Blazor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our goal is to generate idiomatic SDKs that work great in all of these environments. Stainless provides high-quality C# code that takes full advantage of the language, and it feels like our SDKs are hand-crafted by an experienced .NET developer. We include things like strong typing, composition with LINQ, and good tooling support. These are things every C# developer expects.&lt;/p&gt;

&lt;p&gt;It's a high bar, but it's one we managed to hit with the GA release of our C# SDK generator. Since launching our C# beta last summer, we've added more features while improving stability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Full .NET Standard 2.0 support&lt;/li&gt;
&lt;li&gt;Idiomatic types, conversions, and great editor tooling&lt;/li&gt;
&lt;li&gt;Async enumerables for SSE/JSONL and auto-paging&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.stainless.com/docs/guides/configure-strong-naming/" rel="noopener noreferrer"&gt;Strong naming support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Minimal dependencies&lt;/li&gt;
&lt;li&gt;Publishing to NuGet&lt;/li&gt;
&lt;li&gt;File uploads and downloads&lt;/li&gt;
&lt;li&gt;Sensible equality methods&lt;/li&gt;
&lt;li&gt;Built-in generated tests&lt;/li&gt;
&lt;li&gt;The ability to make raw requests when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Some noteworthy C# generator design decisions
&lt;/h2&gt;

&lt;p&gt;Our goal is to generate idiomatic SDKs that feel at home in each language's ecosystem. We also strive to support advanced features and functionality, even if a particular language doesn't have native support for them. This approach shines through in some of the design decisions we made.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;.NET Standard 2.0 support&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The .NET ecosystem is splintered. Lots of people use .NET Standard, which has a limited subset of the full .NET functionality common across various .NET implementations, such as on vanilla Windows, &lt;a href="https://learn.microsoft.com/en-us/windows/uwp/get-started/universal-application-platform-guide" rel="noopener noreferrer"&gt;UWP&lt;/a&gt;, &lt;a href="https://azure.microsoft.com/en-us/products/functions" rel="noopener noreferrer"&gt;Azure Functions&lt;/a&gt; 1.x, &lt;a href="https://dotnet.microsoft.com/en-us/apps/xamarin" rel="noopener noreferrer"&gt;Xamarin&lt;/a&gt;, and many other runtimes. Others use .NET itself and have access to all of the modern features that make C# great. &lt;/p&gt;

&lt;p&gt;The SDKs we generate have wide compatibility and provide the best developer experience possible.  On .NET Standard 2.0, our SDKs are still full-featured, and have good DX even though some language features are missing.&lt;/p&gt;

&lt;p&gt;On .NET 8 and above, our SDKs are fully idiomatic to the language, and take advantage of the available features.&lt;/p&gt;

&lt;p&gt;We accomplished this by considering both stacks carefully as we built our generator. In many cases we were able to accomplish our goals with extensions which allowed us to create record classes that operate as normal classes on .NET Standard. In some cases we rely on preprocessor directives, although this is quite rare, and we ensure we only do this when truly required. As a result, our generated SDKs are still easy to read while supporting both stacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazing unions
&lt;/h3&gt;

&lt;p&gt;Like many languages, C# does not support unions or sum types natively. There are a lot of tradeoffs when designing unions, and we don't have time to get into them all here. The bottom line is that every language has a unique set of constraints, so careful consideration is needed.&lt;/p&gt;

&lt;p&gt;Our solution is for our unions to be backed internally by an &lt;code&gt;object&lt;/code&gt; and a &lt;code&gt;JsonElement&lt;/code&gt;. The &lt;code&gt;object&lt;/code&gt; holds the union variant if one exists, and the &lt;code&gt;JsonElement&lt;/code&gt; holds the raw JSON data from the API. This allows us to represent data that doesn’t match the union perfectly, which can be a problem if the OpenAPI schema deviates from the actual API. We have constructors for each possible variant, as well as an unknown variant constructor, which operates as an escape hatch. This can be useful for bypassing the OpenAPI schema when required.&lt;/p&gt;

&lt;p&gt;To complement the constructors, we create implicit convertors from the variants. This keeps the DX light, as users don't need to wrap all of their unions. We also generate helper functions for switching and matching, which are inspired by the popular &lt;a href="https://github.com/mcintyre321/OneOf" rel="noopener noreferrer"&gt;OneOf .NET library&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Basically, the goal was to make unions easy to use without compromising on type-safety or readability. Let’s take a look at an example below, where we have a Dealership SDK, which contains a &lt;code&gt;Vehicle&lt;/code&gt; union with &lt;code&gt;Car&lt;/code&gt; and &lt;code&gt;Truck&lt;/code&gt; variants.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VehicleConverter&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Vehicle&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&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="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;JsonElement&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_element&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Gets the raw Json value, if the user needs it&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_element&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt;
                &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeToElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&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="c1"&gt;// This property exists on both `Car` and `Truck`, so we&lt;/span&gt;
    &lt;span class="c1"&gt;// automatically bubble it up. We want the user to be able&lt;/span&gt;
    &lt;span class="c1"&gt;// to access it without having to destructure the union&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;WheelsCount&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;car&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WheelsCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;truck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WheelsCount&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="c1"&gt;// For constructing a car variant&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Vehicle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Car&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_element&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// For constructing a truck variant&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Vehicle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Truck&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_element&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// For constructing an unknown variant&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Vehicle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_element&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Helper method for picking a specific variant&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryPickCar&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;NotNullWhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;Car&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Car&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryPickTruck&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;NotNullWhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;Truck&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Truck&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Exhaustive matching with no return type&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Car&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;car&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Truck&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;truck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Car&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;car&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&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;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Truck&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;truck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&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;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DealershipSdkInvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Data did not match any variant of Vehicle"&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="c1"&gt;// Exhaustive matching with return type&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Car&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;car&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Truck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;truck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Car&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;car&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Truck&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;truck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DealershipSdkInvalidDataException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Data did not match any variant of Vehicle"&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="c1"&gt;// These implicit operators are a lighter-weight way&lt;/span&gt;
    &lt;span class="c1"&gt;// to construct the union. Notice that we don't create one&lt;/span&gt;
    &lt;span class="c1"&gt;// for the unknown variant, as that's considered a more advanced&lt;/span&gt;
    &lt;span class="c1"&gt;// feature and in that case the user should be more explicit&lt;/span&gt;
    &lt;span class="c1"&gt;// about what they want.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;implicit&lt;/span&gt; &lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="nf"&gt;Vehicle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Car&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;implicit&lt;/span&gt; &lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="nf"&gt;Vehicle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Truck&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ...snip...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design delivers strong developer experience without sacrificing type safety or readability, and still allows developers to bypass the OpenAPI schema when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immutability by default
&lt;/h3&gt;

&lt;p&gt;Many C# SDKs do not use immutable record classes even though mutability leaves room for logic bugs, thread-safety issues, and potentially sub-optimal performance. Part of making a great SDK is preventing these foot-guns wherever possible, so that developers can focus on building software.&lt;/p&gt;

&lt;p&gt;Our C# SDKs use immutable record classes which are idiomatic and work around the issues above.  Models are record classes, arrays use &lt;code&gt;ImmutableArray&lt;/code&gt;, and dictionaries use &lt;code&gt;FrozenDictionary&lt;/code&gt;. While looking at existing C# SDKs, we found a lot of prior art of people using mutable classes, like &lt;code&gt;List&lt;/code&gt; and &lt;code&gt;Dictionary&lt;/code&gt;, inside of supposedly immutable classes. We believe in a higher bar than this and made sure the data structures we generate are truly immutable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type-safe models with flexibility
&lt;/h3&gt;

&lt;p&gt;A good SDK should make it difficult, but not impossible, to deviate from the OpenAPI spec. Usually, the spec is correct, and we want it to inform the type system and, by extension, the user. Pragmatically, sometimes the API deviates from its spec. Although there are numerous ways to work around this in the &lt;a href="https://www.stainless.com/docs#the-stainless-config" rel="noopener noreferrer"&gt;Stainless config&lt;/a&gt;, sometimes users still need to be able to use the SDK before this happens. This means our data model needs to support some amount of flexibility.&lt;/p&gt;

&lt;p&gt;We work around this by backing our models with an internal read-only dictionary of &lt;code&gt;&amp;lt;string, JsonElement&amp;gt;&lt;/code&gt; which holds the raw data. We immutably expose this to the user so that they can get the raw data if needed. Users can also construct models from their own dictionary, which allows them to bypass the schema when sending requests.&lt;/p&gt;

&lt;p&gt;When the user doesn’t need to bypass the schema, they can use the C# properties we generate to access the model’s data. This makes the experience seamless, as if those properties were the actual backing data themselves.&lt;/p&gt;

&lt;p&gt;Internally, we cache data so that the user doesn’t have to pay the JSON deserialization cost every time they access the same property. We do this in a thread-safe manner so that the end-user doesn’t have to think about it, and our models still appear entirely immutable.&lt;/p&gt;

&lt;p&gt;Here’s what a model might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonModelConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CatFromRaw&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;))]&lt;/span&gt;
&lt;span class="c1"&gt;// JsonModel holds RawData, which is the backing dictionary. It is&lt;/span&gt;
&lt;span class="c1"&gt;// also responsible for caching&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Cat&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JsonModel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Marked as required in the schema&lt;/span&gt;
    &lt;span class="c1"&gt;// (throws exception if missing)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FurColour&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetNotNullClass&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"furColour"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawData&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="s"&gt;"furColour"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&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="c1"&gt;// Marked as optional in the schema&lt;/span&gt;
    &lt;span class="c1"&gt;// (returns null if missing)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;WhiskerCount&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetNullableStruct&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"whiskerCount"&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawData&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="s"&gt;"whiskerCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&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="c1"&gt;// Constructor for object initializer syntax&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Cat&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="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cat&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cat&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="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_rawData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Constructor for bypassing the schema&lt;/span&gt;
&lt;span class="cp"&gt;#pragma warning disable CS8618
&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetsRequiredMembers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FrozenDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_rawData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#pragma warning restore CS8618
&lt;/span&gt;
    &lt;span class="c1"&gt;// Constructor that allows setting all required members as&lt;/span&gt;
    &lt;span class="c1"&gt;// parameters, with the option of also using the&lt;/span&gt;
    &lt;span class="c1"&gt;// object initializer syntax&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetsRequiredMembers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;furColour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FurColour&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;furColour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ... snip ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You would instantiate the object like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Cat&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;FurColour&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WhiskerCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Cat&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Cat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orange"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WhiskerCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trusted by teams in production
&lt;/h2&gt;

&lt;p&gt;The strongest proof of our SDK quality is real teams shipping production systems that depend on them. Here are a few companies using Stainless-generated C# SDKs today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/anthropics/anthropic-sdk-csharp" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/trycourier/courier-csharp" rel="noopener noreferrer"&gt;Courier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/orbcorp/orb-csharp" rel="noopener noreferrer"&gt;Orb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dodopayments/dodopayments-csharp" rel="noopener noreferrer"&gt;Dodo Payments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ArcadeAI/arcade-dotnet" rel="noopener noreferrer"&gt;Arcade AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/browserbase/stagehand-net" rel="noopener noreferrer"&gt;Browserbase&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our goal for this release was to make C# a first-class citizen in the Stainless ecosystem, with SDKs that feel at home in real .NET codebases. Our C# generator reflects that philosophy throughout: runtime-aware code generation, idiomatic models, strong typing with pragmatic escape hatches, and a focus on long-term maintainability over short-term convenience.&lt;/p&gt;

&lt;p&gt;If you’d like to dig deeper, explore the &lt;a href="https://www.stainless.com/changelog/c-sdk-generator-is-generally-available" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; or start generating SDKs today by signing up for a Stainless account.&lt;/p&gt;

&lt;p&gt;We’re excited to see what you build, and we’d love &lt;a href="mailto:support@stainless.com"&gt;feedback&lt;/a&gt; from teams adding C# to their supported languages.&lt;/p&gt;

</description>
      <category>openapi</category>
      <category>stainless</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>AgentLint: A Lighthouse Score for AI-Readiness</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Mon, 16 Feb 2026 15:41:52 +0000</pubDate>
      <link>https://dev.to/cjav_dev/agentlint-a-lighthouse-score-for-ai-readiness-566b</link>
      <guid>https://dev.to/cjav_dev/agentlint-a-lighthouse-score-for-ai-readiness-566b</guid>
      <description>&lt;p&gt;The users of the web are increasingly alien. Not humans. Not traditional crawlers. AI agents like ChatGPT browsing mode, Perplexity, Claude Code and Cowork, are actively navigating websites, reading docs, and trying to make sense of your content. And most sites are completely unprepared for them.&lt;/p&gt;

&lt;p&gt;Your site might look great to a human. It might score 100 on Lighthouse. But when an AI agent shows up, it's probably drowning in nav bars, cookie banners, and markup soup.&lt;/p&gt;

&lt;h2&gt;
  
  
  We've seen this movie before
&lt;/h2&gt;

&lt;p&gt;Think about accessibility. Before a11y tooling existed, making a site accessible felt vague and overwhelming. Then tools like axe and Lighthouse audits came along and made it concrete: here are the issues, here's how to fix them, here's your score. Suddenly accessibility was actionable.&lt;/p&gt;

&lt;p&gt;Same story with internationalization. i18n felt like a massive lift until tooling made it systematic: extract strings, manage translations, lint for hardcoded text.&lt;/p&gt;

&lt;p&gt;And Lighthouse itself turned "make your site fast" from a handwavy goal into a scorecard with specific, fixable items.&lt;/p&gt;

&lt;p&gt;So here's the question: &lt;strong&gt;what's the equivalent for AI-agent readiness?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's what I built AgentLint to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;One command. That's it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @cjavdev/agent-lint https://your-site.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AgentLint crawls your site, runs rules across several categories, and gives you a score from 0 to 100 with a letter grade. It tells you exactly what's working, what's not, and what to fix specific violations.&lt;/p&gt;

&lt;p&gt;The five categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport&lt;/strong&gt; Can agents get your content in formats they prefer? Does your site respond to &lt;code&gt;Accept: text/markdown&lt;/code&gt;? Is your &lt;code&gt;robots.txt&lt;/code&gt; blocking AI crawlers?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure&lt;/strong&gt; Is your HTML well-organized? Do headings follow a logical hierarchy? Do sections have anchor IDs agents can reference?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens&lt;/strong&gt; How efficient is your content for LLM context windows? Are pages bloated with repeated nav and footer content?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discoverability&lt;/strong&gt; Can agents find what they need? Do you have an &lt;code&gt;llms.txt&lt;/code&gt;? A sitemap? An OpenAPI spec?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt; Do you have agent-specific affordances like an MCP manifest or agent usage guides?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get a clean report in the terminal, or pass &lt;code&gt;--json&lt;/code&gt; for structured output you can pipe into CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I kept thinking about what makes a site "agent-friendly" and realized there was no standard way to audit it. Everyone was kind of guessing. Some folks were adding &lt;code&gt;llms.txt&lt;/code&gt; files. Others were serving markdown. But nobody had a checklist, let alone a tool that could run it automatically.&lt;/p&gt;

&lt;p&gt;I wanted something developers could just &lt;em&gt;run&lt;/em&gt; — like Lighthouse, but for this new class of visitor. Point it at a URL, get a score, fix the red items. No config required, no setup, just answers.&lt;/p&gt;

&lt;p&gt;So I built the tool I wished existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that surprise people
&lt;/h2&gt;

&lt;p&gt;Some of the checks are intuitive. Of course you should have a sitemap. But a few tend to catch people off guard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does your site serve markdown when asked?&lt;/strong&gt; If an agent sends &lt;code&gt;Accept: text/markdown&lt;/code&gt;, does your server respond with markdown instead of HTML? Most don't. But this is one of the easiest wins for agent-friendliness — agents &lt;em&gt;much&lt;/em&gt; prefer consuming markdown over parsing HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you have an &lt;code&gt;llms.txt&lt;/code&gt;?&lt;/strong&gt; This is a simple text file at your site root that tells AI models what your site is about, what content is available, and how to navigate it. Think of it like &lt;code&gt;robots.txt&lt;/code&gt; but &lt;em&gt;for&lt;/em&gt; AI rather than about AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are your pages lean enough for context windows?&lt;/strong&gt; LLMs have finite context. If your page dumps 15,000 tokens of nav, sidebar, and footer before getting to the actual content, agents are wasting their context budget on noise. AgentLint flags pages over a configurable token threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much of your content is boilerplate?&lt;/strong&gt; If 40% of every page is the same nav and footer HTML, that's a ton of duplicate tokens agents have to process across pages. AgentLint measures this duplication rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you have an MCP manifest?&lt;/strong&gt; The Model Context Protocol is emerging as a standard for agents to discover and interact with services. AgentLint checks if you've published one at &lt;code&gt;/.well-known/mcp.json&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Seriously, just run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @cjavdev/agent-lint https://your-site.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use it as a skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add cjavdev/agent-lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your site probably already does some of this stuff well. The score will tell you where you stand and what's worth improving. Most of the fixes are straightforward — adding a file here, tweaking a header there.&lt;/p&gt;

&lt;p&gt;The project is open source at &lt;a href="https://github.com/cjavdev/agent-lint" rel="noopener noreferrer"&gt;github.com/cjavdev/agent-lint&lt;/a&gt;. If you have ideas for new rules, find a bug, or just want to share your score, I'd love to hear from you.&lt;/p&gt;

&lt;p&gt;The web is getting new visitors. Let's make sure they feel welcome.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Iterate on your SDKs locally with the Stainless Language Server</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Thu, 12 Feb 2026 20:10:18 +0000</pubDate>
      <link>https://dev.to/stainlessapi/iterate-on-your-sdks-locally-with-the-stainless-language-server-10m0</link>
      <guid>https://dev.to/stainlessapi/iterate-on-your-sdks-locally-with-the-stainless-language-server-10m0</guid>
      <description>&lt;p&gt;Stainless users configure their SDKs with two files: a &lt;code&gt;stainless.yml&lt;/code&gt; config and an OpenAPI spec. Together, these files define how your API maps to generated SDKs, from resource structure and method naming to type definitions and pagination behavior. Getting them right is what makes the difference between a generated SDK that feels hand-written and one that feels auto-generated.&lt;/p&gt;

&lt;p&gt;Until now, editing these files meant working in the Stainless Studio or pushing changes and waiting for build feedback. That workflow works, but there are some limitations. You make a change, trigger a build, wait for diagnostics, fix an issue, and repeat. For quick iterations on your config or spec, that loop is slower than it needs to be.&lt;/p&gt;

&lt;p&gt;Today, we're releasing the &lt;a href="https://www.stainless.com/docs/guides/iterate-with-lsp/" rel="noopener noreferrer"&gt;Stainless Language Server&lt;/a&gt;, which brings the editing capabilities of the Studio directly into your local development environment. If you use VS Code, Cursor, Neovim, Zed, or any editor that supports the Language Server Protocol, you can now get real-time feedback on your Stainless config and OpenAPI spec as you type.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Language Server does
&lt;/h2&gt;

&lt;p&gt;The Stainless Language Server understands the relationship between your &lt;code&gt;stainless.yml&lt;/code&gt; and your OpenAPI spec. It knows which endpoints map to which resources, which schemas are referenced where, and what valid configuration options look like in context. That understanding powers a set of features that make local editing significantly more productive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-time diagnostics and quick fixes
&lt;/h3&gt;

&lt;p&gt;The language server analyzes your Stainless config and OpenAPI spec in real-time, surfacing errors, warnings, and suggestions directly in your editor. This helps you catch issues before running a build. When a diagnostic does appear, common issues come with an autofix you can apply with one click, and warning-level diagnostics can be suppressed with a single action.&lt;/p&gt;

&lt;p&gt;It’s powered by the same diagnostic engine as Studio, so you get identical feedback locally without leaving your editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent completions
&lt;/h3&gt;

&lt;p&gt;When editing your &lt;code&gt;stainless.yml&lt;/code&gt;, the language server offers context-aware autocompletion. It suggests valid config keys, endpoint paths, and schema names drawn from your OpenAPI spec. If you're defining a new resource method, it will suggest available endpoints. If you're referencing a schema, it will complete from the schemas defined in your spec.&lt;/p&gt;

&lt;p&gt;This is especially useful for large APIs. Instead of switching between your config and a long OpenAPI spec to find the right path or schema name, completions surface the options directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhavrskeny7e3i03z31os.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhavrskeny7e3i03z31os.gif" alt="Stainless LSP Autocompletions" width="760" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Go to definition and find all references
&lt;/h3&gt;

&lt;p&gt;You can use "Go to definition" to jump from a reference in your Stainless config straight to the corresponding schema, parameter, or endpoint in your OpenAPI spec. Use "Find all references" to see where a particular schema or endpoint is used across both files.&lt;/p&gt;

&lt;p&gt;For teams working with specs that have hundreds of endpoints, this navigation alone saves significant time. You don't need to manually search through a large YAML file to find where a schema is defined or where an endpoint is referenced.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1c20kwvdqbkzy3ml5ixt.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1c20kwvdqbkzy3ml5ixt.gif" alt="Stainless LSP go-to-definition" width="600" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Codelenses in your OpenAPI spec
&lt;/h3&gt;

&lt;p&gt;Codelenses appear inline in your OpenAPI spec, showing which schemas and endpoints are already configured in your Stainless config. This gives you a quick visual overview of your API's SDK coverage without switching files. You can see at a glance which parts of your spec are represented in your SDKs and which aren't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live transformations
&lt;/h3&gt;

&lt;p&gt;If you use &lt;a href="https://www.stainless.com/blog/stainless-transforms-the-fast-lane-for-fixing-imperfect-openapi-specs" rel="noopener noreferrer"&gt;Stainless Transforms&lt;/a&gt; to reshape your OpenAPI spec, the language server generates and updates your transformed spec in real-time as you edit your config. You can see the effect of each transform immediately, without running a separate build step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting set up
&lt;/h2&gt;

&lt;p&gt;Setup takes a few minutes. Install the Stainless CLI, initialize your workspace, and add the extension for your editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the CLI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap stainless-api/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;stl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Initialize your workspace:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stl init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;.stainless/workspace.json&lt;/code&gt; file that the language server uses to identify your project and locate your config and spec files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the editor extension:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For VS Code and Cursor, search for "Stainless" in the Extensions view and install it. The extension bundles the langauge server, so no separate LSP installation is needed.&lt;/p&gt;

&lt;p&gt;For Neovim and other LSP-compatible editors, install the language server package globally via npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @stainless-api/stainless-language-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure your LSP client to run &lt;code&gt;stainless-language-server --stdio&lt;/code&gt;. The server activates for YAML and JSON files when it detects &lt;code&gt;.stainless/workspace.json&lt;/code&gt; in your project. The &lt;a href="https://www.stainless.com/docs/guides/iterate-with-lsp/#-install-the-extension-for-your-editor" rel="noopener noreferrer"&gt;setup guide&lt;/a&gt; has editor-specific configuration examples.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1wcvfjh7ug9bp6y5zjl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1wcvfjh7ug9bp6y5zjl.gif" alt="Stainless local dev setup" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The local development workflow
&lt;/h2&gt;

&lt;p&gt;With the language server running, the workflow for iterating on your SDK configuration looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your &lt;code&gt;stainless.yml&lt;/code&gt; and OpenAPI spec in your editor&lt;/li&gt;
&lt;li&gt;Edit your config with completions and inline validation guiding you&lt;/li&gt;
&lt;li&gt;See diagnostics in real-time as you type, catching misconfigurations before they reach a build&lt;/li&gt;
&lt;li&gt;Use go-to-definition to navigate between your config and spec&lt;/li&gt;
&lt;li&gt;Review codelenses to check SDK coverage across your API&lt;/li&gt;
&lt;li&gt;When you're satisfied, push your changes to Stainless for a full build&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same set of features available in the Stainless Studio, now running locally. For teams that prefer working in their own editor, or that want to use AI coding tools like Claude or Cursor alongside their Stainless config, the language server makes that possible.&lt;/p&gt;

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

&lt;p&gt;The Stainless Language Server is available now. Install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=stainless.vscode-stainless" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt; or the &lt;a href="https://www.npmjs.com/package/@stainless-api/stainless-language-server" rel="noopener noreferrer"&gt;npm package&lt;/a&gt; and check out the &lt;a href="https://www.stainless.com/docs/guides/iterate-with-lsp/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for detailed setup instructions. If you run into issues or have feedback, reach out to &lt;a href="mailto:support@stainless.com"&gt;support@stainless.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>lsp</category>
      <category>stainless</category>
      <category>openapi</category>
    </item>
    <item>
      <title>Stainless CLI generator: your API, now with `--help`</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Mon, 09 Feb 2026 16:14:14 +0000</pubDate>
      <link>https://dev.to/stainlessapi/stainless-cli-generator-your-api-now-with-help-13hp</link>
      <guid>https://dev.to/stainlessapi/stainless-cli-generator-your-api-now-with-help-13hp</guid>
      <description>&lt;p&gt;You can now generate a command line tool for your API directly from your OpenAPI spec with Stainless. Enable the CLI target in the Studio to generate a command line interface with proper argument parsing, automatic pagination, and the documentation features developers expect.&lt;/p&gt;

&lt;p&gt;Command line tools are one of the fastest ways for developers (and agents!) to interact with APIs.  They're scriptable, composable, and ideal for quick experimentation and iteration.  Building a high-quality CLI tool that people actually want to use, however, requires a lot of work.  You need to get a lot of things right, like output formatting, pagination, shell completions, manpages, cross-platform distribution, and more.  Due to the amount of time and effort that’s normally required, many API providers either don’t provide a CLI or only provide a minimal, watered-down tool that developers find wanting.&lt;/p&gt;

&lt;p&gt;With today's release, Stainless makes supporting the CLI easier than ever.  You can generate a CLI tool from the same spec that powers your SDKs and keep everything in sync automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go, CLI, Go!
&lt;/h2&gt;

&lt;p&gt;Under the hood, when we generate a CLI tool for your API, it’s actually a wrapper around your Go SDK.&lt;/p&gt;

&lt;p&gt;A common question among our earliest CLI users was, “why Go?” It’s a great question! We chose Go for several reasons, but most of them boil down to the high performance and ease of distribution Go provides.&lt;/p&gt;

&lt;p&gt;Go compiles to native binaries with no external runtime dependencies and near-instant startup time. Your users don't have to have Go installed, all they see is a fast, efficient, and native executable.&lt;/p&gt;

&lt;p&gt;Go also has excellent cross-compilation support, which makes it easy for you to ship builds for macOS, Linux, and Windows across multiple architectures. That makes distribution straightforward for you, and installation easy for your users.&lt;/p&gt;

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

&lt;p&gt;Every CLI tool we generate follows a resource-based structure that mirrors your API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;your-project &lt;span class="o"&gt;[&lt;/span&gt;resource &lt;span class="o"&gt;[&lt;/span&gt;sub-resource...]] method-name &lt;span class="nt"&gt;--method-arg&lt;/span&gt; value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, with an API that manages people, using the CLI would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Retrieve a person by ID&lt;/span&gt;
your-project people retrieve &lt;span class="nt"&gt;--id&lt;/span&gt; 123

&lt;span class="c"&gt;# Create a new person&lt;/span&gt;
your-project people create &lt;span class="nt"&gt;--job&lt;/span&gt; &lt;span class="s2"&gt;"President"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt;.full-name &lt;span class="s2"&gt;"Abraham Lincoln"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt;.nickname &lt;span class="s2"&gt;"Abe Lincoln"&lt;/span&gt;

&lt;span class="c"&gt;# List all people (paginated automatically)&lt;/span&gt;
your-project people list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Method names and flags use kebab-case, the &lt;code&gt;--help&lt;/code&gt; flag gives you full documentation for any command, and man pages are generated automatically so &lt;code&gt;man your-project&lt;/code&gt; will work out of the box for your end users.&lt;/p&gt;

&lt;p&gt;Shell completions are included for Bash, Zsh, fish, and PowerShell, and the CLI also works on Windows using the standard &lt;code&gt;--flag=value&lt;/code&gt; syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Argumentative arguments
&lt;/h2&gt;

&lt;p&gt;Argument parsing is one of those problems that looks simple until you try to create a polished implementation. Shell scripting has many quirks that make complex payloads awkward. JSON requires nested quoting that's easy to mess up. If you want tab completion and proper &lt;code&gt;--help&lt;/code&gt; documentation, your flags need to be defined statically (which rules out infinitely nested paths like &lt;code&gt;--messages.0.content.0.text&lt;/code&gt;).And so on.&lt;/p&gt;

&lt;p&gt;We spent a lot of time working through the tradeoffs. The design we landed on optimizes common cases, but handles complex structures with grace when you need them.&lt;/p&gt;

&lt;p&gt;Pass simple values with intuitive syntax familiar to command line users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;your-project &lt;span class="nb"&gt;users &lt;/span&gt;create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Alice"&lt;/span&gt; &lt;span class="nt"&gt;--role&lt;/span&gt; admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use dot notation for nested objects up to two levels deep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;your-project people create &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt;.first &lt;span class="s2"&gt;"Abraham"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt;.last &lt;span class="s2"&gt;"Lincoln"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--address&lt;/span&gt;.city &lt;span class="s2"&gt;"Springfield"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For anything deeper, or for complex polymorphic types, pass YAML or JSON inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# YAML&lt;/span&gt;
  your-project people create &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s1"&gt;'first: Abraham, last: Lincoln'&lt;/span&gt;

  &lt;span class="c"&gt;# JSON&lt;/span&gt;
  your-project people create &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s1"&gt;'{"first": "Abraham", "last": "Lincoln"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass array values with repeated flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;your-project &lt;span class="nb"&gt;users &lt;/span&gt;update &lt;span class="nt"&gt;--tag&lt;/span&gt; admin &lt;span class="nt"&gt;--tag&lt;/span&gt; reviewer &lt;span class="nt"&gt;--tag&lt;/span&gt; owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipe full payloads via stdin. JSON and YAML both work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;your-project people create &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;YAML&lt;/span&gt;&lt;span class="sh"&gt;
name:
  full_name: Abraham Lincoln
  nickname: Honest Abe
job: President
&lt;/span&gt;&lt;span class="no"&gt;YAML
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also combine piped input with flags. Flags override values from stdin, which is useful for templating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;base-user.json | your-project &lt;span class="nb"&gt;users &lt;/span&gt;create &lt;span class="nt"&gt;--role&lt;/span&gt; admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Built for AI agents
&lt;/h2&gt;

&lt;p&gt;CLI tools make great interfaces for agents.&lt;/p&gt;

&lt;p&gt;When an agent uses raw HTTP, it has to construct headers, manage authentication, serialize request bodies, and parse responses. A CLI takes care of all of that: authentication is handled using environment variables, request bodies become flags, and responses come back as structured JSON.  All by default!&lt;/p&gt;

&lt;p&gt;And, perhaps most critically, a CLI tool is self-documenting. An agent can run &lt;code&gt;--help&lt;/code&gt; to discover what's available.&lt;/p&gt;

&lt;p&gt;We tested this with a CLI generated for the Spotify API. Claude Code ran &lt;code&gt;--help&lt;/code&gt; to discover the available commands, constructed valid requests, and parsed the responses without needing external documentation.&lt;/p&gt;

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

&lt;p&gt;The same properties that make CLIs good for humans also apply to agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable structure: &lt;code&gt;resource method --flag value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Self-documenting: &lt;code&gt;--help&lt;/code&gt; on every command&lt;/li&gt;
&lt;li&gt;Good error messages: clear feedback when something's wrong and suggestions for mistyped command names or flags&lt;/li&gt;
&lt;li&gt;Standard conventions: kebab-case flags, JSON output, meaningful exit codes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Robust distribution
&lt;/h2&gt;

&lt;p&gt;Building a binary is one thing, but distributing it across platforms, setting up package managers, and keeping versions in sync with your API changes is quite another.&lt;/p&gt;

&lt;p&gt;The Stainless CLI generator plugs into the same release flow as your other Stainless SDKs. When you merge a release PR, we use &lt;a href="https://goreleaser.com" rel="noopener noreferrer"&gt;GoReleaser&lt;/a&gt; to automatically build binaries for macOS (arm64 and amd64), Linux (arm64, amd64, 386), and Windows (arm64, amd64, 386), and each release publishes to GitHub with all compiled binaries.&lt;/p&gt;

&lt;p&gt;For macOS users, we support Homebrew out of the box. Configure your tap repository and Stainless keeps the formula updated automatically. Your users can install your CLI with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;your-org/tools/your-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're also working on support for more package managers, like npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started today
&lt;/h2&gt;

&lt;p&gt;Enable the CLI target in your Stainless config alongside your Go SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;cli&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;binary_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out our &lt;a href="https://www.stainless.com/docs/guides/cli-generator" rel="noopener noreferrer"&gt;CLI generator docs&lt;/a&gt; for more details on setup and configuration. If you have any feedback, please reach out to &lt;a href="mailto:support@stainless.com"&gt;support@stainless.com&lt;/a&gt;. We'd love to hear what you think!&lt;/p&gt;

</description>
      <category>stainless</category>
      <category>cli</category>
      <category>openapi</category>
    </item>
    <item>
      <title>How to generate a TypeScript SDK from your OpenAPI spec</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Mon, 09 Feb 2026 15:47:45 +0000</pubDate>
      <link>https://dev.to/stainlessapi/how-to-generate-a-typescript-sdk-from-your-openapi-spec-4m8j</link>
      <guid>https://dev.to/stainlessapi/how-to-generate-a-typescript-sdk-from-your-openapi-spec-4m8j</guid>
      <description>&lt;p&gt;If you maintain a public API, you've probably faced the same question: how do you help developers (and agents) integrate quickly without forcing them to hand-craft HTTP requests, parse JSON responses, and handle edge cases on their own?&lt;/p&gt;

&lt;p&gt;The answer is a platform, not a single artifact. SDKs, documentation, and other tools work together to shape how developers experience your API. &lt;/p&gt;

&lt;p&gt;This guide focuses on one component: the TypeScript SDK. For JavaScript and TypeScript developers, it is often the deciding factor between choosing your API or moving on to a competitor with better tooling.&lt;/p&gt;

&lt;p&gt;This guide covers how to generate a TypeScript SDK from an OpenAPI specification. It covers the traditional approaches, where they fall short, and how to produce a production-ready SDK that feels hand-crafted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a TypeScript SDK matters for your API
&lt;/h2&gt;

&lt;p&gt;Without an SDK, developers must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read your API documentation and manually construct HTTP requests&lt;/li&gt;
&lt;li&gt;Handle authentication headers, query parameter serialization, and request body formatting&lt;/li&gt;
&lt;li&gt;Parse responses and map them to their own types&lt;/li&gt;
&lt;li&gt;Implement error handling, retries, and pagination logic from scratch&lt;/li&gt;
&lt;li&gt;Keep their integration code updated as your API evolves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A well-designed TypeScript SDK eliminates all of this. Developers get type-safe methods with autocomplete, compile-time error checking, and built-in handling for common patterns.&lt;/p&gt;

&lt;p&gt;Integration time drops significantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqsf75xf3hxcehkafjhl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqsf75xf3hxcehkafjhl.png" alt="Raw fetch vs. using an SDK" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The payoff is faster adoption and fewer support tickets, especially when something goes wrong. Strong TypeScript types make it easier for developers and agents to detect, understand, and recover from incorrect usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The traditional approach: OpenAPI Generator
&lt;/h2&gt;

&lt;p&gt;The legacy starting point is the open-source OpenAPI Generator. It supports dozens of languages, including TypeScript, and can scaffold a client library from your spec in minutes.&lt;/p&gt;

&lt;p&gt;Here's the typical workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a TypeScript SDK using the fetch template&lt;/span&gt;
openapi-generator-cli generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-i&lt;/span&gt; path/to/openapi.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-g&lt;/span&gt; typescript-fetch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ./generated-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a functional SDK with TypeScript interfaces for your models and a client class with methods for each endpoint.&lt;/p&gt;

&lt;p&gt;For internal tools or quick prototypes, this works. For a public-facing SDK that represents your product, the limitations become apparent quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where basic code generation falls short
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Generic, non-idiomatic code.&lt;/strong&gt; The templates produce functional output, but method names, class structures, and patterns don't always match what TypeScript developers expect. The code feels generated, not designed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing advanced features.&lt;/strong&gt; Real APIs need pagination, retries with exponential backoff, and structured error handling. OpenAPI Generator doesn't include these. Your users must implement them manually, which leads to inconsistent behavior across integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual maintenance burden.&lt;/strong&gt; Every time your API changes, you re-run the generator, manually bump versions, and publish to &lt;code&gt;npm&lt;/code&gt;. There's no built-in handling for breaking changes or automated release workflows. The onus is entirely on your team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limited customization.&lt;/strong&gt; Tweaking the output requires forking Mustache templates, which quickly becomes a maintenance project of its own.&lt;/p&gt;

&lt;p&gt;We designed Stainless for teams that run into these limits. It takes the same OpenAPI input, but optimizes for production SDKs rather than scaffolding. The table below compares the two approaches.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;OpenAPI Generator&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Stainless&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic (&lt;code&gt;ResponseError&lt;/code&gt;, &lt;code&gt;FetchError&lt;/code&gt;, &lt;code&gt;RequiredError&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Rich hierarchy with many specific error types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None built-in&lt;/td&gt;
&lt;td&gt;Exponential backoff with jitter, Retry-After support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pagination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Returns raw paginated response&lt;/td&gt;
&lt;td&gt;Auto-pagination with async iterators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming (SSE)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Full SSE parser with &lt;code&gt;Stream&lt;/code&gt; class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Ergonomics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verbose method names, single object params&lt;/td&gt;
&lt;td&gt;Clean method names, positional + optional params&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type Safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Good (&lt;code&gt;FromJSON&lt;/code&gt; / &lt;code&gt;ToJSON&lt;/code&gt; transforms)&lt;/td&gt;
&lt;td&gt;Excellent (strict mode, exactOptionalPropertyTypes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Middleware Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre/post/onError middleware&lt;/td&gt;
&lt;td&gt;Request options per-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File Uploads&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic FormData/Blob&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Uploadable&lt;/code&gt; type (&lt;code&gt;File&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt;, &lt;code&gt;ReadStream&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JSDoc from OpenAPI spec&lt;/td&gt;
&lt;td&gt;Rich TSDoc with code examples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Raw Response Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via ApiResponse wrapper&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.asResponse()&lt;/code&gt; and &lt;code&gt;.withResponse()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment Config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;basePath&lt;/code&gt; in Configuration&lt;/td&gt;
&lt;td&gt;Multiple named environments and env vars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Module System&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single export pattern&lt;/td&gt;
&lt;td&gt;Dual ESM/CJS with conditional exports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to generate a TypeScript SDK from OpenAPI with Stainless: the recommended approach
&lt;/h2&gt;

&lt;p&gt;Stainless generates TypeScript SDKs from OpenAPI that follow idiomatic language conventions and include production features like retries, pagination, and structured errors by default.&lt;/p&gt;

&lt;p&gt;This approach comes from experience building large-scale SDK platforms, including work on Stripe’s SDK infrastructure, where maintaining high-quality, hand-crafted SDKs across many languages required far more than mapping endpoints to methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started
&lt;/h3&gt;

&lt;p&gt;You can create a new project from an OpenAPI spec in about one minute:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign up for Stainless.&lt;/strong&gt; Create an account at &lt;a href="http://app.stainless.com" rel="noopener noreferrer"&gt;app.stainless.com&lt;/a&gt; using your GitHub account, then create an organization for yourself or your company.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a project.&lt;/strong&gt; Create a new Stainless project for your API by providing an OpenAPI spec. You can paste a URL to a hosted spec, upload a file, or start from an example such as the Petstore API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: A Stainless &lt;strong&gt;project&lt;/strong&gt; represents a single API. From one project, you can generate and manage multiple SDKs in different languages, along with related artifacts such as documentation sites, CLIs, MCP servers, or Terraform providers, all driven from the same OpenAPI spec.&lt;/p&gt;

&lt;p&gt;Once you create the project project, Stainless generates a TypeScript SDK and opens it in the Studio inside the dashboard. Stainless also invites you to a GitHub repository containing the generated SDK code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feoa6hflpy6vlwsfsxg83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feoa6hflpy6vlwsfsxg83.png" alt="Stainless studio for viewing an SDK" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What you get out of the box
&lt;/h3&gt;

&lt;p&gt;The generated TypeScript SDK includes retry logic, streaming, pagination, and error handling that you would otherwise build and maintain yourself. Here's what that looks like in practice, using the &lt;a href="https://github.com/openai/openai-node" rel="noopener noreferrer"&gt;OpenAI Node SDK&lt;/a&gt; as an example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero dependencies&lt;/strong&gt;. Stainless uses the built-in &lt;code&gt;fetch&lt;/code&gt; API for HTTP requests. The SDK works in Node.js, Deno, Bun, Cloudflare Workers, Vercel Edge Runtime, and modern browsers without pulling in a single dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full type safety&lt;/strong&gt;. Every model, request, and response is fully typed. Developers get autocomplete for all parameters and compile-time validation of their API calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Uses OPENAI_API_KEY env var by default&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a coding assistant that talks like a pirate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Are semicolons optional in JavaScript?&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Streaming (SSE)&lt;/strong&gt;. For APIs that support Server-Sent Events, the SDK provides a &lt;code&gt;Stream&lt;/code&gt; class with async iteration built in. No manual SSE parsing required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Say "Sheep sleep deep" ten times fast!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auto-pagination&lt;/strong&gt;. List endpoints return async iterators that handle page tokens automatically. Developers don't need to write pagination logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Automatically fetches more pages as needed.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &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;job&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fineTuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Or work with a single page at a time:&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fineTuning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;for &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;job&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;page&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Automatic retries&lt;/strong&gt;. The SDK automatically retries for connection errors, &lt;code&gt;408&lt;/code&gt; timeouts, &lt;code&gt;429&lt;/code&gt; rate limits, and &lt;code&gt;5xx&lt;/code&gt; server errors with exponential backoff. Developers can configure this globally or per-request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Configure the default for all requests:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default is 2&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Or override per-request:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-5.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello!&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&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;h3&gt;
  
  
  Configuring, publishing, and keeping SDKs in sync
&lt;/h3&gt;

&lt;p&gt;Beyond what's generated out of the box, Stainless handles the operational side of SDK management:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SDK configuration&lt;/strong&gt;. A &lt;code&gt;stainless.yaml&lt;/code&gt; file controls how your API maps to SDK methods: resource grouping, pagination schemes, method naming, and more. The dashboard includes an LSP-powered editor with autocomplete and &lt;a href="https://stainless.com/docs/guides/configure" rel="noopener noreferrer"&gt;inline documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated publishing&lt;/strong&gt;. Connect your &lt;code&gt;npm&lt;/code&gt; account and new SDK versions publish automatically when your API spec changes. Stainless &lt;a href="https://stainless.com/docs/guides/publish" rel="noopener noreferrer"&gt;supports&lt;/a&gt; secure OIDC-based trusted publishing through GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automated regeneration&lt;/strong&gt;. A GitHub Action in your API repository &lt;a href="https://stainless.com/docs/guides/preview-builds/" rel="noopener noreferrer"&gt;keeps SDKs in sync automatically&lt;/a&gt;: open a PR that changes your spec and Stainless previews the SDK diff; merge it and release PRs are opened across all your SDK repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world examples
&lt;/h2&gt;

&lt;p&gt;Stainless generates the official, production SDKs for several major API providers, collectively handling millions of API calls daily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/openai/openai-node" rel="noopener noreferrer"&gt;OpenAI Node SDK&lt;/a&gt;: millions of weekly downloads&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/anthropic-sdk-typescript" rel="noopener noreferrer"&gt;Anthropic TypeScript SDK&lt;/a&gt;: powers Claude integrations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/lithic-com/lithic-node" rel="noopener noreferrer"&gt;Lithic Node SDK&lt;/a&gt;: card issuing and payment processing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Modern-Treasury/modern-treasury-node" rel="noopener noreferrer"&gt;Modern Treasury Node SDK&lt;/a&gt;: payment operations platform&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Basic code generation tools work for internal tools or prototypes. For a public SDK that represents your product, you need production-ready features: zero dependencies, full type safety, built-in pagination and retries, and automated publishing.&lt;/p&gt;

&lt;p&gt;Stainless generates TypeScript SDKs with these features built in. The result is faster adoption, fewer support tickets, and a stronger API platform.&lt;/p&gt;

&lt;p&gt;Ready to generate your TypeScript SDK? &lt;a href="https://app.stainless.com/" rel="noopener noreferrer"&gt;Get started with Stainless&lt;/a&gt; or &lt;a href="https://stainless.com/docs/targets/typescript" rel="noopener noreferrer"&gt;read the documentation&lt;/a&gt; to learn more.&lt;/p&gt;

</description>
      <category>openapi</category>
      <category>typescript</category>
      <category>stainless</category>
    </item>
    <item>
      <title>Is the VCR plugged in? Common Sense Troubleshooting For Web Devs</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Wed, 10 Jul 2024 16:41:17 +0000</pubDate>
      <link>https://dev.to/cjav_dev/is-the-vcr-plugged-in-common-sense-troubleshooting-for-web-devs-78o</link>
      <guid>https://dev.to/cjav_dev/is-the-vcr-plugged-in-common-sense-troubleshooting-for-web-devs-78o</guid>
      <description>&lt;p&gt;The summers of 6th through 8th grade were incredible. I grew up in North Lake Tahoe and would ride bikes with my brothers and neighborhood kids from daybreak til the streetlights came on. We'd make home videos of us attempting BMX tricks (think Baby-Gap version of the X-Games). We recorded using our family's mini-DV camcorder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzU3LCJwdXIiOiJibG9iX2lkIn19--4b2d5ac2203891599fef99c95388ce69dd4561d5%2Fcj%2520bmx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzU3LCJwdXIiOiJibG9iX2lkIn19--4b2d5ac2203891599fef99c95388ce69dd4561d5%2Fcj%2520bmx.jpg" alt="Sick grind, bro"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dad would help connect the camcorder to the VCR and the TV and walk through the labyrinth of VRC remote buttons and menus so that we could transfer the videos to VHS. In the summer of 1997, my dad shared a critically important process with me that I've never forgotten and has been an invaluable framework for troubleshooting ever since. He said, "If the VCR isn't working, you should first check if it's plugged in. Start from the most foundational component (the wall receptacle), then follow the path to the next component in the chain. Ideally, isolating each component and link will ensure all pieces are working as expected. Intuit how the underlying systems work and how they interact."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzU4LCJwdXIiOiJibG9iX2lkIn19--252f2915620f60cb18de0d114b1e490477a560d0%2Fvcr-remotes.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzU4LCJwdXIiOiJibG9iX2lkIn19--252f2915620f60cb18de0d114b1e490477a560d0%2Fvcr-remotes.jpg" alt="vcr-remotes.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See, Dad is a genius-level engineer. He ran a commercial refrigeration and appliance business for my entire childhood (which I was lucky enough to learn from). He built houses from the ground up, reincarnated cars, and was a master of all things mechanical, electrical, woodworking, and (to his chagrin) plumbing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzU5LCJwdXIiOiJibG9iX2lkIn19--4f025106cec18127351ff0331ba5ce101b235781%2Fice-machine.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzU5LCJwdXIiOiJibG9iX2lkIn19--4f025106cec18127351ff0331ba5ce101b235781%2Fice-machine.webp" alt="ice-machine.webp"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Customers would call and say, "My ice machine isn't making ice." Several times I'd go on jobs with him and watch him use a multimeter to test the voltages at the wall, the switch, the fans, the compressors. Is the electrical all working as expected? What about the water supply? He'd check the inlet valves, pressure, float, and door switches. Is the evaporator working as expected? Are the evaporator coils all frosty, are the condenser coils transferring heat as expected, and so-on. What about the control board? He'd isolate the problem to a single component and replace or repair it. He'd test the system and iterate until it harvested ice as expected.&lt;/p&gt;

&lt;p&gt;Once you've isolated a specific component, you can repeat the same process for it itself. Is the component getting power? Is it receiving and/or sending the right signals that are compatible with the other components in the system? Is it damaged?&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools for Troubleshooting
&lt;/h2&gt;

&lt;p&gt;Dad has a truck full of tools: multimeters, manifold gauges, infrared thermometers (laser guns!). You too, will benefit from collecting tools to help you isolate and identify problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYwLCJwdXIiOiJibG9iX2lkIn19--0582d2e4535688dd622f86cd58ef5c1bf8b9ac56%2Fmultimeter.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYwLCJwdXIiOiJibG9iX2lkIn19--0582d2e4535688dd622f86cd58ef5c1bf8b9ac56%2Fmultimeter.jpg" alt="multimeter.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYxLCJwdXIiOiJibG9iX2lkIn19--787b6baa2c8985fcbd22378393b0ad693e8fac71%2Fmanifold-guages.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYxLCJwdXIiOiJibG9iX2lkIn19--787b6baa2c8985fcbd22378393b0ad693e8fac71%2Fmanifold-guages.jpg" alt="manifold-guages.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In 2011, I spent the year in Afghanistan building tactical networks that ran over fiber optics, satellite, ethernet, and radio. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYyLCJwdXIiOiJibG9iX2lkIn19--f0a899b05a42f1bd12c4ff6ac35595a1784dd64e%2Fafghanistan-2011-08-10.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYyLCJwdXIiOiJibG9iX2lkIn19--f0a899b05a42f1bd12c4ff6ac35595a1784dd64e%2Fafghanistan-2011-08-10.jpg" alt="afghanistan-2011-08-10.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot of my job was configuring Cisco routers and switches. The best troubleshooting tools for that job were &lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;traceroute&lt;/code&gt;, &lt;code&gt;nslookup&lt;/code&gt;, &lt;code&gt;dig&lt;/code&gt; and &lt;code&gt;show&lt;/code&gt; commands. I'd start by pinging the next hop, the next hop, and the next hop. If the pings were successful, I'd traceroute to the destination to see if the packets followed the expected path. If the packets were not following the expected path, I'd use the show commands to inspect the routing tables and check if RIP, OSPF, and BGP were doing their job correctly. Check the interface status and the logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For web development, the best troubleshooting tools are:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.  Logs and Print Statements&lt;/strong&gt;: &lt;/p&gt;

&lt;p&gt;Logs are the most important tool for troubleshooting. Logs are like the black box on an airplane. They record everything that happens in your application. They can tell you what happened, when, and sometimes why it happened. Logs are the first place you should look when something goes wrong. One school of thought is to add tons of print statements to your code to help you understand what's happening. Learn &lt;code&gt;puts&lt;/code&gt; &lt;code&gt;p&lt;/code&gt; and &lt;code&gt;JSON.pretty_generate&lt;/code&gt; in Ruby and the &lt;code&gt;console.log&lt;/code&gt; in JavaScript to print out the state of variables and the code flow. Also, use your own logging and learn about log levels to get the right information into your production logs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYzLCJwdXIiOiJibG9iX2lkIn19--9dd8f67cc07dc678d5a4a8908031c35d57c7d79d%2Flogs-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzYzLCJwdXIiOiJibG9iX2lkIn19--9dd8f67cc07dc678d5a4a8908031c35d57c7d79d%2Flogs-1.png" alt="logs-1.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY0LCJwdXIiOiJibG9iX2lkIn19--999a63edd902b93ec88501c543c18f24ee823d99%2Flogs-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY0LCJwdXIiOiJibG9iX2lkIn19--999a63edd902b93ec88501c543c18f24ee823d99%2Flogs-2.png" alt="logs-2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Debuggers&lt;/strong&gt;: &lt;/p&gt;

&lt;p&gt;Debuggers are tools that allow you to pause the execution of your code and inspect the state of your application. You can set breakpoints in your code and step through it line by line. Debuggers are useful for understanding how your code executes and identifying the bugs' root cause. Some like print statements, and some like debuggers. I like both. I'm kinda old school for debugging ruby, and I like to use &lt;code&gt;byebug&lt;/code&gt;. In JavaScript, I like to use &lt;code&gt;debugger&lt;/code&gt; or the browser's built-in break statements.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;continue&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt; and &lt;code&gt;puts&lt;/code&gt; and &lt;code&gt;l=&lt;/code&gt; to print where you are again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'byebug'&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;byebug&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;my_cool_math_thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;byebug&lt;/span&gt;
  &lt;span class="n"&gt;answer1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;answer1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;my_cool_math_thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY1LCJwdXIiOiJibG9iX2lkIn19--bc4d94806687dfdbf7256ec7fb12a67b512e5979%2Fbyebug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY1LCJwdXIiOiJibG9iX2lkIn19--bc4d94806687dfdbf7256ec7fb12a67b512e5979%2Fbyebug.png" alt="byebug.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY2LCJwdXIiOiJibG9iX2lkIn19--2522041a2f70dcaed91a4c90dcc403c1b48b5ba4%2Fchrome-debugger-real.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY2LCJwdXIiOiJibG9iX2lkIn19--2522041a2f70dcaed91a4c90dcc403c1b48b5ba4%2Fchrome-debugger-real.png" alt="chrome-debugger-real.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Browser Developer Tools&lt;/strong&gt;: Browser developer tools are built into every modern web browser. They allow you to inspect the HTML, CSS, and JavaScript of a web page, as well as monitor network requests, view console logs, and debug JavaScript. Browser developer tools are essential for debugging client-side issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. REPLs&lt;/strong&gt;: REPL stands for Read-Eval-Print Loop. A REPL is an interactive programming environment that allows you to write and execute code in real time. REPLs are useful for quickly testing code snippets and exploring language features. You can use the console in your browser's developer tools as a client side REPL and your server side framework likely has a built in REPL (e.g. &lt;code&gt;rails console&lt;/code&gt;). Using the REPL, you can experiment with bits of code quickly to see how they behave.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6Nzc0LCJwdXIiOiJibG9iX2lkIn19--64c5568cd071f918adbd208b588c5babeac94751%2Frepl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6Nzc0LCJwdXIiOiJibG9iX2lkIn19--64c5568cd071f918adbd208b588c5babeac94751%2Frepl.png" alt="repl.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Automated Tests&lt;/strong&gt;: Unit tests are automated tests that verify the behavior of a small unit of code in isolation. I like to write unit tests for every bug reported by a user. This way, I can reproduce the bug in a controlled environment and verify that the fix works as expected and that we wont see a regression. There are many different JavaScript test frameworks like &lt;a href="https://jestjs.io/" rel="noopener noreferrer"&gt;Jest&lt;/a&gt;, &lt;a href="https://go.cypress.io/" rel="noopener noreferrer"&gt;cypress&lt;/a&gt;, &lt;a href="https://mochajs.org/" rel="noopener noreferrer"&gt;mocha&lt;/a&gt;, and &lt;a href="https://jasmine.github.io/" rel="noopener noreferrer"&gt;jasmine&lt;/a&gt;. We use &lt;a href="https://rspec.info/" rel="noopener noreferrer"&gt;Rspec&lt;/a&gt; and &lt;a href="https://github.com/minitest/minitest" rel="noopener noreferrer"&gt;Minitest&lt;/a&gt; for unit and integration tests in our rails application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-bisect" rel="noopener noreferrer"&gt;&lt;strong&gt;6. Git Bisect&lt;/strong&gt;&lt;/a&gt;: I've only used this a handful of times in my career, but it can be very handy to identify when a bug was introduced. You start by telling git that the current commit is bad and an earlier commit is good. Git will then checkout a commit in the middle of the range. You test the commit and tell git if it's good or bad. Git will then checkout a commit halfway between the last and current commit. You repeat this process until you find the commit that introduced the bug. It's sort of binary search through history to find a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Static Code Analysis&lt;/strong&gt;: Static code analysis tools analyze your code without executing it. They can identify potential bugs, security vulnerabilities, and code smells. I like to use linters like &lt;a href="https://rubocop.org/" rel="noopener noreferrer"&gt;Rubocop&lt;/a&gt; and &lt;a href="https://eslint.org/" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt; to catch syntax errors and enforce code style.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY4LCJwdXIiOiJibG9iX2lkIn19--8b7dbc2462339433b7560dd6a1186a162f0c1670%2Flinter.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY4LCJwdXIiOiJibG9iX2lkIn19--8b7dbc2462339433b7560dd6a1186a162f0c1670%2Flinter.png" alt="linter.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Error Messages and Stack Traces&lt;/strong&gt;: Unfortunately, most error messages look like the villain in a bad action movie. They are scary! Big red letters, often with cryptic language. But, error messages are your best friend. They tell you what went wrong and where it went wrong. They can be cryptic, but they often contain valuable information that can help you identify the root cause of a bug. Stack traces are like a map of the failed code execution path. They show you the sequence of function calls that led to the error. This is your map of all the components that need to be checked -- in order! Usually, I trust that third-party code is working as expected (that's like the wall receptacle, power is probably on, but sometimes you need to check the breaker panel). Do yourself a favor and set up some error monitoring and alerting tools in production so you know when an error happens for a user. We use Sentry at work and I've used Rollbar in the past.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY3LCJwdXIiOiJibG9iX2lkIn19--f8d6ac7e8fb296ee3bd97d4674cca5f24339950b%2Ferror-message.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY3LCJwdXIiOiJibG9iX2lkIn19--f8d6ac7e8fb296ee3bd97d4674cca5f24339950b%2Ferror-message.png" alt="error-message.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Database Management Tools&lt;/strong&gt;: If you're working with a database, you'll have tools to manage and query the database. Web dev is often CRUD (Create, Read, Update, Delete) data from a database. The database is often the lowest common denominator in the stack. You can use a database management tool to inspect the data in the database, run queries, and check for errors. You can also use the database management tool to check the status of the database server and ensure that it's running as expected. Learn enough SQL to be able to answer some basic questions -- (I still recommend &lt;a href="https://sqlzoo.net/wiki/SQL_Tutorial" rel="noopener noreferrer"&gt;SQL Zoo!&lt;/a&gt;). If you're using rails &lt;code&gt;rails dbconsole&lt;/code&gt; is a shortcut to log into your database interface and start querying. Recently, I've been wrestling with some Redis memory issues and have found the &lt;code&gt;redis-cli&lt;/code&gt; to be invaluable and have learned just enough to be dangerous.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many users have an email with &lt;code&gt;test&lt;/code&gt; in it? &lt;/li&gt;
&lt;li&gt;What does the &lt;code&gt;weather_days&lt;/code&gt; table look like?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY5LCJwdXIiOiJibG9iX2lkIn19--df495568db64d82474974e48fb59e113afd9c571%2Fdbms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzY5LCJwdXIiOiJibG9iX2lkIn19--df495568db64d82474974e48fb59e113afd9c571%2Fdbms.png" alt="dbms.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some &lt;code&gt;redis-cli&lt;/code&gt; basics, for ya!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzcwLCJwdXIiOiJibG9iX2lkIn19--1930ba915ca8b5cc2f6d6e7a451de84e5dc78201%2Fredis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzcwLCJwdXIiOiJibG9iX2lkIn19--1930ba915ca8b5cc2f6d6e7a451de84e5dc78201%2Fredis.png" alt="redis.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. LLMs and StackOverflow&lt;/strong&gt;: Learning to ask the right questions of your search engine (Google, GitHub, or StackOverflow) or LLM chat tool like ChatGPT or Claude can help you find solutions faster. The more mature your stack, the more likely there are to be other people who've run into the exact same problem you have and asked about it or created issues on GitHub. As &lt;a href="https://www.facebook.com/thedailyshow/videos/jon-stewart-on-ai-as-a-replacement-tool/267769313072024/" rel="noopener noreferrer"&gt;Jon Stuart jokes about prompt engineering&lt;/a&gt;, it can pay to become a good "types-question-guy."&lt;/p&gt;

&lt;h2&gt;
  
  
  Combat Lifesaver Course
&lt;/h2&gt;

&lt;p&gt;In Army basic training, we learned several basic life-saving skills like CPR, applying tourniquets, and treating for shock. In the heat of the moment, it's easy to forget the basics and get lost in the chaos. Many topics in basic training use absurdly simple frameworks so that you can remember them in the heat of the moment. For example, when you find a casualty, you're supposed to remember the ABCs: Airway, Breathing, Circulation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzcxLCJwdXIiOiJibG9iX2lkIn19--d1f2da360d8ab24c5969f65cc2b8aeb582eff2d6%2Fbls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzcxLCJwdXIiOiJibG9iX2lkIn19--d1f2da360d8ab24c5969f65cc2b8aeb582eff2d6%2Fbls.png" alt="Photo credit: https://www.researchgate.net/publication/221818120_Initial_assessment_and_treatment_with_the_Airway_Breathing_Circulation_Disability_Exposure_ABCDE_approach "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;. Check their airway, is it clear? If not, clear it.&lt;br&gt;
  &lt;strong&gt;B&lt;/strong&gt;. Check their breathing, are they breathing? If not, give them mouth-to-mouth.&lt;br&gt;
  &lt;strong&gt;C&lt;/strong&gt;. Check their circulation, do they have a pulse? If not, stop the bleeding (apply a tourniquet) and elevate their legs.&lt;/p&gt;

&lt;p&gt;Simple frameworks are easy to remember and can be applied in the heat of the moment. They're also easy to teach and can be applied to various problems. When your site goes down, what are your ABCs?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side rant&lt;/strong&gt;: shouldn’t basic life-saving courses be free to the public?! Someone make that happen!&lt;/p&gt;

&lt;h2&gt;
  
  
  Accelerated Learning Through Troubleshooting
&lt;/h2&gt;

&lt;p&gt;In software engineering, encountering and resolving bugs is not merely an inconvenient part of the development process; it is a critical component that drives rapid skill enhancement and professional growth. I have a theory that the frequency and variety of bugs you encounter directly correlate with the speed at which a software engineer improves. My weaker theory is that this also leads to writing better, more durable code.&lt;/p&gt;

&lt;p&gt;As an App Academy instructor, I was lucky enough to accelerate my learning by supporting web development students. While working through the curriculum, they would encounter new bugs and errors that would stump them and other TAs, and I'd get to help find and solve bugs. This experience multiplexed the opportunities for me to encounter new and different (sometimes creative and bewildering) errors and spend all day troubleshooting instead of simply building on my own and only encountering the bugs I created. I learned 6-8 times as much in about 2 years than I had in the previous 5 combined.&lt;/p&gt;

&lt;p&gt;Diverse problems required varied approaches and solutions (e.g. comment out half the code), which broadened my problem-solving toolkit.&lt;/p&gt;

&lt;p&gt;It deepened my understanding -- I &lt;em&gt;had&lt;/em&gt; to understand how different components interact, leading to a more profound grasp of the entire stack.&lt;/p&gt;

&lt;p&gt;I learned to anticipate potential issues and write more robust, maintainable code to avoid similar bugs in the future.&lt;/p&gt;

&lt;p&gt;The experience built intuition and pattern recognition, helping identify root causes quickly.&lt;/p&gt;

&lt;p&gt;For instance, do you want to know the most common root cause for bugs in web applications? SPELLNIG!&lt;/p&gt;

&lt;p&gt;Because spelling is one of the most common errors, I prefer to use &lt;code&gt;barewords&lt;/code&gt; over &lt;code&gt;@instance_variables&lt;/code&gt; in Ruby (&lt;a href="https://graceful.dev/courses/the-freebies/modules/ruby-language/topic/episode-004-barewords/" rel="noopener noreferrer"&gt;thanks Avdi&lt;/a&gt;).  This way, if I misspell a variable, Ruby will raise an error instead of creating a new variable. Also, a decent reason to use TypeScript over JavaScript and linting tools like &lt;a href="https://www.npmjs.com/package/eslint-plugin-spellcheck" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt;. This is a simple example of how I've learned to write more robust code through troubleshooting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips and Tricks
&lt;/h2&gt;

&lt;p&gt;Walk the path of the request and response - this is the way. Start from the most foundational component (the client) and follow the path to the next component in the chain (the server). Ideally, isolate each component and link to ensure all pieces work as expected. Intuit how the underlying systems work and how they interact with each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Client&lt;/strong&gt;: Is the client sending the request as expected? Is the request being sent to the correct endpoint? Is the request being sent with the correct headers and body?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make a change to the client code, refresh the page, and verify that you’re changing the code you think you are. (common error is making a change locally but then looking at prod or something).&lt;/li&gt;
&lt;li&gt;Add console log statements to the top of the JavaScript files you expect to be executing, are those printed to the dev console?&lt;/li&gt;
&lt;li&gt;Is the code to execute the request as expected? Use the browser's debugger to find the code that makes the request. Set breakpoints and inspect the request object before it's sent.&lt;/li&gt;
&lt;li&gt;Use the browser's developer tools to inspect the request and response.&lt;/li&gt;
&lt;li&gt;Look in the server logs to see if the request is being received.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Server&lt;/strong&gt;: Is the server receiving the request as expected? Is the server sending the response as expected?&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Look in the server logs to see if the request and the expected URL, headers, and request body are being received.
- Is the expected server code executing?
  - Is the route defined that knows how to pick which code should execute based on the request path?
  - Is the code that should be executing actually executing?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3. Anywhere:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Follow the control flow, is the execution following the right branches of conditionals?&lt;/li&gt;
&lt;li&gt;Are methods being called with the expected arguments?&lt;/li&gt;
&lt;li&gt;Did the function return the expected value?&lt;/li&gt;
&lt;li&gt;Are the types of objects the expected types?&lt;/li&gt;
&lt;li&gt;Are responses from APIs and Libraries formatted as your code expects?&lt;/li&gt;
&lt;li&gt;Are credentials and constants and environments what you expect?&lt;/li&gt;
&lt;li&gt;Should the message be passed to the class or an instance?&lt;/li&gt;
&lt;li&gt;Is this pointing at what you expect in JavaScript?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Call Your Shots
&lt;/h2&gt;

&lt;p&gt;In pool (like the billiards kind), the badasses call their shots by naming which pocket and which balls will fall &lt;em&gt;before&lt;/em&gt; they hit the cue ball. In software engineering, you should say what you expect to happen before you refresh the page or run a test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzcyLCJwdXIiOiJibG9iX2lkIn19--a120e777dcd2b0ad28418c22a379ac2da900bf33%2Fcall-your-shot.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzcyLCJwdXIiOiJibG9iX2lkIn19--a120e777dcd2b0ad28418c22a379ac2da900bf33%2Fcall-your-shot.jpg" alt="call-your-shot.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If all this sounds obvious, you've been blessed with a good teacher at some point. But, it's also easy to forget when you're in the thick of a problem. It's easy to forget to check if the VCR is plugged in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzczLCJwdXIiOiJibG9iX2lkIn19--8fee1d2b209b7aeaf38648b6f505a82f11f179e0%2Fplug-it-in.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Frails%2Factive_storage%2Fblobs%2FeyJfcmFpbHMiOnsiZGF0YSI6NzczLCJwdXIiOiJibG9iX2lkIn19--8fee1d2b209b7aeaf38648b6f505a82f11f179e0%2Fplug-it-in.jpg" alt="plug-it-in.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Offer free trials without an upfront payment method using Stripe Checkout</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Wed, 15 Feb 2023 19:13:37 +0000</pubDate>
      <link>https://dev.to/4thzoa/offer-free-trials-without-an-upfront-payment-method-using-stripe-checkout-253a</link>
      <guid>https://dev.to/4thzoa/offer-free-trials-without-an-upfront-payment-method-using-stripe-checkout-253a</guid>
      <description>&lt;p&gt;Follow along to learn how to use Stripe Checkout to collect a customer's information and start a free trial &lt;strong&gt;without&lt;/strong&gt; requiring payment details. Let's get started!&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/x9Grjc-8tbw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Today we're going to start from a Stripe Sample, a prebuilt example with some simple views, a couple of server routes, and a webhook handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe samples create checkout-single-subscription trials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll use Ruby for our server, but you should be able to follow along in your favorite server-side language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Use the arrow keys to navigate: ↓ ↑ → ← 
? What server would you like to use: 
    java
    node
    php
    python
↓ ▸ ruby
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the root of our new &lt;code&gt;trials&lt;/code&gt; directory, you'll notice a file called &lt;code&gt;sample-seed.json&lt;/code&gt;. We can use the Stripe CLI to execute the API calls in this seed fixture to create products and prices. If you haven’t already installed the Stripe CLI, follow the &lt;a href="https://stripe.com/docs/stripe-cli#install" rel="noopener noreferrer"&gt;instructions in the documentation&lt;/a&gt;, then run the fixture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe fixtures sample-seed.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  New products: starter and professional
&lt;/h2&gt;

&lt;p&gt;That fixture creates two new products called &lt;code&gt;starter&lt;/code&gt; and &lt;code&gt;professional&lt;/code&gt;. Each product has 2 related Prices – one for monthly and one for annual. From the &lt;a href="https://dashboard.stripe.com/test/products?active=true" rel="noopener noreferrer"&gt;Stripe Dashboard&lt;/a&gt;, we can grab the monthly Price ID for the &lt;code&gt;starter&lt;/code&gt; product. This price is configured to collect $12 per month, and we’ll pass this as an argument to the API call when creating the Checkout Session. Want to learn more about modeling your business with Products and Prices? Have a look at this &lt;a href="https://dev.to/stripe/modeling-your-saas-business-with-products-and-prices-59e0"&gt;past article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F01-price-copy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F01-price-copy.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we can open up our server directory and update the &lt;code&gt;.env&lt;/code&gt; file with our new Price IDs. The &lt;code&gt;.env&lt;/code&gt; file should look like this but have your Price IDs and API keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;BASIC_PRICE_ID="price_1MbOQMCZ6qsJgndJy04BjDxe"&lt;/span&gt;
&lt;span class="s"&gt;DOMAIN="http://localhost:4242"&lt;/span&gt;
&lt;span class="s"&gt;PRO_PRICE_ID="price_1MbOQOCZ6qsJgndJrpCrN3KK"&lt;/span&gt;
&lt;span class="s"&gt;STATIC_DIR="../client"&lt;/span&gt;
&lt;span class="s"&gt;STRIPE_PUBLISHABLE_KEY="pk_test_vAZ3g..."&lt;/span&gt;
&lt;span class="s"&gt;STRIPE_SECRET_KEY="rk_test_51Ece..."&lt;/span&gt;
&lt;span class="s"&gt;STRIPE_WEBHOOK_SECRET="whsec_9d75cc1016..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll install dependencies for the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can start the Sinatra server with &lt;code&gt;ruby server.rb&lt;/code&gt; and visit &lt;a href="http://localhost:4242" rel="noopener noreferrer"&gt;localhost:4242&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F02-landing.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F02-landing.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Selecting the starter plan redirects the customer to Stripe Checkout, where they will enter payment details. Notice that nothing about this page signals that we're on a trial yet. That's because we have not modified the params for creating the Checkout Session to start a trial without payment method upfront.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F03-without-trial.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F03-without-trial.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Checkout Session for trials
&lt;/h2&gt;

&lt;p&gt;From the &lt;code&gt;/create-checkout-session&lt;/code&gt; route, we're creating a Checkout Session and redirecting. We have an entire Checkout 101 series for you to get up to speed quickly. It's available in the &lt;a href="https://stripe.com/docs/videos/checkout-101" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; or &lt;a href="https://www.youtube.com/watch?v=TJCdUYQTLJU&amp;amp;list=PLy1nL-pvL2M5cO2i3lSYtwyqZh3EeGR9L" rel="noopener noreferrer"&gt;here on YouTube&lt;/a&gt;. Here’s the code for reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/create-checkout-session'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Checkout&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;success_url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOMAIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/success.html?session_id={CHECKOUT_SESSION_ID}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;cancel_url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOMAIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/canceled.html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="s1"&gt;'subscription'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;line_items: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'priceId'&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="k"&gt;rescue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
    &lt;span class="n"&gt;halt&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;redirect&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;303&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we create the Checkout Session, we can pass a hash into &lt;a href="https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-subscription_data" rel="noopener noreferrer"&gt;&lt;code&gt;subscription_data&lt;/code&gt;&lt;/a&gt;. This allows us to configure the subscription created by Stripe Checkout. Inside of &lt;code&gt;subscription_data&lt;/code&gt; we can set &lt;code&gt;trial_period_days&lt;/code&gt; to an integer number of days that we want to offer a trial. We also want to set &lt;code&gt;payment_method_collection&lt;/code&gt; to &lt;code&gt;if_required&lt;/code&gt; so we don’t require payment details upfront.&lt;/p&gt;

&lt;p&gt;Here’s the new API call for creating Checkout Sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Checkout&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;success_url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOMAIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/success.html?session_id={CHECKOUT_SESSION_ID}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;cancel_url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOMAIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/canceled.html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="s1"&gt;'subscription'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;line_items: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'priceId'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="ss"&gt;subscription_data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;trial_period_days: &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="ss"&gt;payment_method_collection: &lt;/span&gt;&lt;span class="s1"&gt;'if_required'&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;Now, the button to start a subscription on the starter plan redirects customers to the Stripe-hosted checkout page. Rather than needing to enter payment details upfront, customers only need to enter their email address. This starts a free 14-day trial and then collects $12 per month after that, assuming the customer sets up payment details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F04-with-trial.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F04-with-trial.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I recommend using the &lt;a href="https://stripe.com/docs/customer-management/integrate-customer-portal" rel="noopener noreferrer"&gt;customer portal&lt;/a&gt; to enable customers to add and update payment methods on file. To see the customer portal in action, click the manage billing button on the success page after subscribing. The &lt;a href="https://stripe.com/docs/api/customer_portal/sessions/create" rel="noopener noreferrer"&gt;API call to create a customer portal session&lt;/a&gt; is simple; you need only specify the customer. I prefer storing the ID of the customer alongside the authenticated user, but you can also pull the customer’s ID from the Checkout Session object directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BillingPortal&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="n"&gt;checkout_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;return_url: &lt;/span&gt;&lt;span class="n"&gt;return_url&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;# Redirect to session.url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Emailing customers when trials end
&lt;/h2&gt;

&lt;p&gt;At the end of a trial, you’ll want the customer to convert to paid and enter their payment details. One way to encourage customers to come back onto your site to enter their card is to email them just before the trial ends. Stripe offers a feature to automatically email customers when trials end with the Billing scale plan. Sign up for Billing scale and configure your email settings &lt;a href="https://dashboard.stripe.com/settings/billing/automatic" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Alternatively, you can listen for the &lt;code&gt;trial_will_end&lt;/code&gt; webhook notification and send your email with a link to the customer portal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F05-settings.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwo-trial%2F05-settings.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll find this view in your Stripe dashboard under &lt;a href="https://dashboard.stripe.com/test/settings/billing/portal" rel="noopener noreferrer"&gt;Settings &amp;gt; Customer portal&lt;/a&gt;. Customers can follow the customer portal link URL to manage their billing from the portal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/webhook'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# You can use webhooks to receive information about asynchronous payment events.&lt;/span&gt;
  &lt;span class="c1"&gt;# For more about our webhook events check out https://stripe.com/docs/webhooks.&lt;/span&gt;
  &lt;span class="n"&gt;webhook_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'STRIPE_WEBHOOK_SECRET'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;webhook_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
    &lt;span class="c1"&gt;# Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured.&lt;/span&gt;
    &lt;span class="n"&gt;sig_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_STRIPE_SIGNATURE'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

    &lt;span class="k"&gt;begin&lt;/span&gt;
      &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;construct_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig_header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;webhook_secret&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ParserError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="c1"&gt;# Invalid payload&lt;/span&gt;
      &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SignatureVerificationError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="c1"&gt;# Invalid signature&lt;/span&gt;
      &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'⚠️  Webhook signature verification failed.'&lt;/span&gt;
      &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;symbolize_names: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;construct_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'customer.subscription.trial_will_end'&lt;/span&gt;
    &lt;span class="n"&gt;customer_portal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://billing.stripe.com/p/login/test_7sIcQT9yjgqxewEdQQ"&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Email customer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer_portal&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;content_type&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;
  &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might be wondering how we can test that our billing logic will work as expected at the end of the 14-day trial. &lt;a href="https://stripe.com/docs/billing/testing/test-clocks" rel="noopener noreferrer"&gt;Test Clocks&lt;/a&gt; are purpose-built for testing these scenarios. To learn more about Test Clocks, check out &lt;a href="https://www.youtube.com/watch?v=0GsVXfDPavg" rel="noopener noreferrer"&gt;this video&lt;/a&gt;. This snippet shows how you would create a test clock, create a new customer with reference to the clock, then use that customer with the Checkout Session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;    &lt;span class="n"&gt;test_clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestHelpers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestClock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;frozen_time: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;test_clock: &lt;/span&gt;&lt;span class="n"&gt;test_clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stripe&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Checkout&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;customer: &lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;success_url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOMAIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/success.html?session_id={CHECKOUT_SESSION_ID}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;cancel_url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'DOMAIN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/canceled.html'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# mode: 'subscription',&lt;/span&gt;
      &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="s1"&gt;'payment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;line_items: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# price: params['priceId'],&lt;/span&gt;
        &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="s1"&gt;'price_1MRMnoCZ6qsJgndJJ9JrzPgs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="ss"&gt;subscription_data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;trial_period_days: &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;payment_method_collection: &lt;/span&gt;&lt;span class="s1"&gt;'if_required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Now you know how to offer free trials without payment methods upfront using Stripe Checkout. There are several benefits to this approach, chief among them &lt;strong&gt;increased conversion&lt;/strong&gt; because of a lower bar of entry. You might also want to s&lt;a href="https://stripe.com/docs/billing/subscriptions/trials#configure-free-trials-without-payment-methods-to-cancel" rel="noopener noreferrer"&gt;pecify whether to cancel or pause the subscription if the customer didn’t provide a payment method during the trial period&lt;/a&gt;. No matter what your subscription use case, you can now build it with Checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" alt="CJ Avilla" width="229" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cjav.dev" rel="noopener noreferrer"&gt;CJ Avilla&lt;/a&gt; (&lt;a href="https://twitter.com/cjav_dev" rel="noopener noreferrer"&gt;@cjav_dev&lt;/a&gt;) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a &lt;a href="https://www.youtube.com/CJAvilla" rel="noopener noreferrer"&gt;YouTuber&lt;/a&gt;. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲. &lt;/p&gt;

</description>
      <category>stripe</category>
      <category>payments</category>
      <category>tutorial</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Handling webhooks for Stripe Connect</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Thu, 26 Jan 2023 05:00:00 +0000</pubDate>
      <link>https://dev.to/4thzoa/handling-webhooks-for-stripe-connect-4ikg</link>
      <guid>https://dev.to/4thzoa/handling-webhooks-for-stripe-connect-4ikg</guid>
      <description>&lt;p&gt;Did you know that you can build automations that trigger when events happen on your Stripe account? These automations are typically implemented by listening for webhook event notifications (POST requests) sent to a special route on your server called a webhook handler. While not strictly required, it’s a best practice to use webhooks for many parts of a Stripe Connect integration, including onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;We’ll use the term “platform account” or “platform” to talk about the Stripe Account that you will use to build your application. A “connected account” is another Stripe Account for your user that you will be facilitating payments on behalf of. We’ll also talk about “end-customers,” the customers buying from your users. To learn more about picking the right connected account types, or charge flows (direct vs. destination vs. separate charge and transfer), see the previous articles in this series. &lt;/p&gt;

&lt;h2&gt;
  
  
  Differences between direct account webhooks and Connect webhooks
&lt;/h2&gt;

&lt;p&gt;To set up a webhook handler, you'll add a route to your server, then create a webhook endpoint in Stripe that points at your public endpoint. There are two types of webhook endpoints that you can configure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Direct or Account webhook endpoints (events from your platform account)&lt;/li&gt;
&lt;li&gt;Connect webhook endpoints (events from accounts connected to your platform)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are some nuanced differences between the two:&lt;/p&gt;

&lt;p&gt;Webhook event notifications for Connect events include the related account’s ID in the payload, but payloads for Direct endpoints do not include your own platform account ID. Here’s an example of a Connect event, which you can tell by the &lt;code&gt;account&lt;/code&gt; field being present:&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;"evt_mCtVEgOX48Guwy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"livemode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acct_HzrXqpz4WWwqUx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pending_webhooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1349654313&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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="err"&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;h2&gt;
  
  
  How to create a Connect webhook handler
&lt;/h2&gt;

&lt;p&gt;You’ll likely need to set up two separate webhook endpoints in Stripe for your integration. You can either create two separate routes on your server (e.g., &lt;code&gt;/webhooks&lt;/code&gt; and &lt;code&gt;/connect-webhooks&lt;/code&gt;) or use the same route that handles event notifications for both webhook event types. You only create one Connect endpoint for all connected accounts on your platform – no need to create a new endpoint for each connected account. In this article, we’ll focus on the Connect details. If you’re new to webhooks check out the &lt;a href="https://stripe.com/docs/videos/developer-foundations?video=webhook-helpers" rel="noopener noreferrer"&gt;videos&lt;/a&gt; and find an &lt;a href="https://dashboard.stripe.com/test/webhooks/create" rel="noopener noreferrer"&gt;example webhook endpoint implementation in the dashboard&lt;/a&gt; when creating a new webhook endpoint. &lt;/p&gt;

&lt;p&gt;Note that each webhook endpoint you create has its own unique webhook signing secret to verify the POST request came from Stripe’s servers. We’ll see later that using the Stripe CLI for development will give you a special shared webhook signing secret that works for two endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let’s look at a couple of examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a new Payout is created, a &lt;code&gt;payout.created&lt;/code&gt; event fires, and the payload includes an &lt;code&gt;arrival_date&lt;/code&gt;. We can build an automation to email the connected account holders to let them know when to expect their payout to be deposited. We’ll handle this event in our Connect webhook handler.&lt;/li&gt;
&lt;li&gt;When a new Payment is collected from an end-customer, a &lt;code&gt;payment_intent.succeeded&lt;/code&gt; event fires, letting us know we need to fulfill the order. Assuming we’re using destination charges, this fires on your platform account. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 1 - Implement with two separate routes
&lt;/h3&gt;

&lt;p&gt;This approach is simple and gives you good separation between your Connect webhook event handling logic and your account webhook event handling logic.&lt;/p&gt;

&lt;p&gt;Both live and testmode events are sent to your production Connect webhook endpoints. This is so you can build test workflows for connected accounts that are connected through live mode. This means you should always check the livemode property of the event notification to ensure the event is in the correct mode.&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="s1"&gt;/connect-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&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="s1"&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;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endpointSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_YOUR_CONNECT_ENDPOINT_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify webhook signature and extract the event.&lt;/span&gt;
  &lt;span class="c1"&gt;// See https://stripe.com/docs/webhooks/signatures for more information.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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;request&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;endpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// We only want to run these automations for livemode…&lt;/span&gt;
  &lt;span class="k"&gt;if &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;livemode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;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;payout.created&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payout&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&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;connectedAccountId&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;account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;sendPayoutComingSoonEmailToConnectedAccountUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectedAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payout&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;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="s1"&gt;/webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&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="s1"&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;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endpointSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_YOUR_DIRECT_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify webhook signature and extract the event.&lt;/span&gt;
  &lt;span class="c1"&gt;// See https://stripe.com/docs/webhooks/signatures for more information.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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;request&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;endpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &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;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;payment_intent.succeeded&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// event.account is undefined since this is an event from a direct webhook endpoint.&lt;/span&gt;
    &lt;span class="nf"&gt;fulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentIntent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2 - Reuse the same route for both webhook event types
&lt;/h3&gt;

&lt;p&gt;It can be convenient to reuse the same webhook endpoint for all Stripe use-cases. You might even want to reuse the same webhook endpoint across multiple third-party integrations. I want to share one trick for verifying webhook signing secrets when you have multiple: nesting verification for the second signature in the catch block of the first.&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="s1"&gt;/webhooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&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="s1"&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;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectEndpointSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_YOUR_CONNECT_ENDPOINT_SECRET&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;directEndpointSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_YOUR_DIRECT_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify webhook signature and extract the event.&lt;/span&gt;
  &lt;span class="c1"&gt;// See https://stripe.com/docs/webhooks/signatures for more information.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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;request&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;connectEndpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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;StripeSignatureVerificationError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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;request&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;directEndpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// We only want to run these automations for livemode...&lt;/span&gt;
  &lt;span class="k"&gt;if &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;livemode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;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;payout.created&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payout&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&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;connectedAccountId&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;account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;sendPayoutComingSoonEmailToConnectedAccountUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectedAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payout&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="k"&gt;if &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;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;payment_intent.succeeded&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentIntent&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// event.account is undefined since this is an event from a direct webhook endpoint.&lt;/span&gt;
    &lt;span class="nf"&gt;fulfillOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paymentIntent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to build and test webhooks locally with the Stripe CLI
&lt;/h2&gt;

&lt;p&gt;For Stripe to send a POST request to your server, you need a publicly accessible URL. There are a couple of alternatives when working on building and testing webhooks locally. You could use a tunneling tool like &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;, but we recommend using the &lt;a href="https://stripe.com/docs/stripe-cli" rel="noopener noreferrer"&gt;Stripe CLI&lt;/a&gt;, a purpose-built tool for helping with all aspects of building your integration. &lt;/p&gt;

&lt;p&gt;The Stripe CLI comes with the &lt;code&gt;listen&lt;/code&gt; command that creates a direct connection between your local running server and Stripe. When events fire on your Stripe account (or connected accounts) those are forwarded to your local running webserver. &lt;/p&gt;

&lt;p&gt;To get started with the Stripe CLI, first &lt;a href="https://stripe.com/docs/stripe-cli#install" rel="noopener noreferrer"&gt;install&lt;/a&gt; it, then run &lt;code&gt;stripe login&lt;/code&gt; to authenticate with your Stripe Account.&lt;/p&gt;

&lt;p&gt;Then, run the &lt;code&gt;listen&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:4242/webhook &lt;span class="nt"&gt;--forward-connect-to&lt;/span&gt; localhost:4242/connect-webhook -–latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;forward-to&lt;/code&gt; specifies the URL for the &lt;em&gt;direct&lt;/em&gt; webhook handler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forward-connect-to&lt;/code&gt; specifies the URL for the &lt;em&gt;connect&lt;/em&gt; webhook handler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are re-using the same route for both direct and connect webhook events, then you would run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:4242/webhook &lt;span class="nt"&gt;--forward-connect-to&lt;/span&gt; localhost:4242/webhook -–latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;latest&lt;/code&gt; tells the CLI to create a webhook endpoint that generates events based on the most recent API version. This is important when using strongly typed SDKs like stripe-java and stripe-dotnet which are pinned to specific API versions. By default, a webhook endpoint will be created with your Stripe Account’s default API version. If that is different from the API version your SDK is pinned to, then the SDK will not be able to properly deserialize the event payload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That starts a listener and prints out the webhook signing secret:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwebhooks%2F01-stripe-cli-listen-output.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwebhooks%2F01-stripe-cli-listen-output.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my case, the signing secret is &lt;code&gt;whsec_9d75cc10168ca3f518f64a69a5015bc07222290a199b27985efe350c7c59ecde&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To see events fire, we can either take actions in our application or the Stripe dashboard that result in the event types we want to test. A simpler and more streamlined approach to testing is to use the Stripe CLI’s built-in fixtures via the &lt;code&gt;trigger&lt;/code&gt; command. &lt;code&gt;trigger&lt;/code&gt; runs one or many API calls to Stripe to result in a given event firing and subsequently being delivered to your webhook handlers.&lt;/p&gt;

&lt;p&gt;We can trigger the payment_intent.succeeded event on our platform account like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe trigger payment_intent.succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates and confirms a payment intent. Notice the logs under the stripe listen output show us the HTTP status code, request method and URL, and the ID of the events firing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwebhooks%2F02-trigger-without-account.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwebhooks%2F02-trigger-without-account.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can also trigger events on connected accounts by passing the &lt;code&gt;--stripe-account&lt;/code&gt; option with the ID of the connected account. Here’s an example for triggering a &lt;code&gt;payout.created&lt;/code&gt; event for a connected account with ID &lt;code&gt;acct_1KPRf02R4eodYxfv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe trigger payout.created &lt;span class="nt"&gt;--stripe-account&lt;/span&gt; acct_1KPRf02R4eodYxfv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwebhooks%2F03-trigger-with-account.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fwebhooks%2F03-trigger-with-account.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also use the &lt;a href="https://stripe.com/docs/stripe-vscode" rel="noopener noreferrer"&gt;Stripe for VS Code extension&lt;/a&gt;, a thin UI wrapper around the Stripe CLI. &lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Store the event payload
&lt;/h3&gt;

&lt;p&gt;One of the best practices when working with webhooks is to store the event payloads for later use. This is especially useful when working with Stripe Connect webhooks, as it allows you to re-process the events in case of errors or if you need to debug a specific event. I like to store event payloads in a database table with the raw payload in a JSON column and a few useful columns like processing status, processing errors, event ID, event source, and Stripe account ID.&lt;/p&gt;

&lt;p&gt;By keeping a record of all events, you will have a historical record of all events that occurred in your platform, which gives you a full understanding of the events that have taken place, regardless of the outcome of the webhook event handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Only subscribe to events that you're using today
&lt;/h3&gt;

&lt;p&gt;To improve your integration's performance, only subscribe to the webhook events you need, not all of them. This reduces the number of requests your webhook handler must handle. Also, if your server does not respond with 2XX status codes, Stripe will retry sending events for several days. This could result in a DDOS by the &lt;a href="https://en.wikipedia.org/wiki/Thundering_herd_problem" rel="noopener noreferrer"&gt;thundering herd&lt;/a&gt; of events received when your server comes back online.&lt;/p&gt;

&lt;p&gt;You can easily subscribe to the events you need by selecting them in the webhooks settings in your Stripe Dashboard, or by using the Stripe API to configure your webhook endpoints. It's also important to keep in mind that webhook events can change over time, so it's a good idea to periodically review your webhook event subscriptions to ensure that you're still handling the events that are important to your integration.&lt;/p&gt;

&lt;p&gt;You can even filter for specific event types when using the Stripe CLI listen command using the &lt;code&gt;--events&lt;/code&gt; flag: \&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stripe listen &lt;span class="nt"&gt;--forward-to&lt;/span&gt; localhost:4242/webhook &lt;span class="nt"&gt;--forward-connect-to&lt;/span&gt; localhost:4242/webhook &lt;span class="nt"&gt;--latest&lt;/span&gt; &lt;span class="nt"&gt;--events&lt;/span&gt; checkout.session.completed,account.updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Respond immediately and then process the event with a background job
&lt;/h3&gt;

&lt;p&gt;When building your webhook handler, it's important to respond immediately to the webhook event notification to avoid timing out and decrease latency. One approach is to immediately respond to the webhook event notification with a 200 status code, and then process the event in a background job. This allows your webhook handler to quickly acknowledge receipt of the event and free up resources to handle other incoming requests, while the background job can take the time needed to process the event.&lt;/p&gt;

&lt;p&gt;Background jobs are especially useful when the event processing requires heavy computation, calls to external services, or database operations, which can take longer to complete. Additionally, it also allows you to handle events asynchronously, which improves the scalability and fault-tolerance of your webhook handler. There are several libraries available, like Bull, Celery, etc., which can help you set up background jobs in your application.&lt;/p&gt;

&lt;p&gt;Here’s an example using &lt;code&gt;bull&lt;/code&gt; with a redis server to queue and process Stripe webhook event notifications in the background. We’ll start by creating a queue and giving it a function that will execute for each event, then when we receive a request to the &lt;code&gt;webhook&lt;/code&gt; endpoint, we’ll enqueue a new event to be processed with &lt;code&gt;Q.add()&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bull&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;redisHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&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;redisPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;6379&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;queueName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe_webhooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A queue for the jobs scheduled based on a routine without any external requests&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queueName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redisPort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redisHost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &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;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// We only want to run these automations for livemode...&lt;/span&gt;
  &lt;span class="k"&gt;if &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;livemode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &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;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;payout.created&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payout&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&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;connectedAccountId&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;account&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;sendPayoutComingSoonEmailToConnectedAccountUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectedAccountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payout&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="k"&gt;if &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;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;payment_intent.succeeded&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;💰 Payment captured!&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment_intent.payment_failed&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;❌ Payment failed.&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;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;t3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&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="s1"&gt;/webhooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bodyParser&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="s1"&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectEndpointSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_YOUR_CONNECT_ENDPOINT_SECRET&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;directEndpointSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whsec_YOUR_DIRECT_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Verify webhook signature and extract the event.&lt;/span&gt;
  &lt;span class="c1"&gt;// See https://stripe.com/docs/webhooks/signatures for more information.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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;request&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;connectEndpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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;StripeSignatureVerificationError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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;request&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;directEndpointSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, you learned how to build automations with Stripe Connect webhooks. You read how to test and develop locally using Stripe CLI's listen command, and how to verify webhook signatures to secure your webhook endpoint. If you’re following along with the series, we’ll use our new Stripe Connect webhook handlers in a future edition about onboarding connected accounts, so stay tuned!&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" alt="CJ Avilla" width="229" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cjav.dev" rel="noopener noreferrer"&gt;CJ Avilla&lt;/a&gt; (&lt;a href="https://twitter.com/cjav_dev" rel="noopener noreferrer"&gt;@cjav_dev&lt;/a&gt;) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a &lt;a href="https://www.youtube.com/CJAvilla" rel="noopener noreferrer"&gt;YouTuber&lt;/a&gt;. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲. &lt;/p&gt;

</description>
      <category>stripe</category>
      <category>webhooks</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Taking a cut with Stripe Connect</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Tue, 24 Jan 2023 05:00:00 +0000</pubDate>
      <link>https://dev.to/4thzoa/taking-a-cut-with-stripe-connect-1kjk</link>
      <guid>https://dev.to/4thzoa/taking-a-cut-with-stripe-connect-1kjk</guid>
      <description>&lt;p&gt;There are several ways to take a cut, or collect a commission on the payments flowing through your Stripe integration. In the previous articles in this series, you learned &lt;a href="https://www.cjav.dev/articles/picking-the-right-charge-type-for-your-stripe-connect-platform" rel="noopener noreferrer"&gt;how to decide between Standard, Express, and Custom accounts types&lt;/a&gt;, and about the different funds flows (also known as charge types) that you can implement with Stripe Connect. Here, you’ll learn approaches to collecting a portion of the proceeds as revenue for your platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are four primary ways to earn money with your platform:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Collecting application fees with direct or destination charges&lt;/li&gt;
&lt;li&gt;Withholding a small amount of the payment when transferring funds to the connected account with destination charges or separate charges and transfers&lt;/li&gt;
&lt;li&gt;Use Account debits to collect a one-off payment from a connected account&lt;/li&gt;
&lt;li&gt;Collecting a recurring payment from your users as customers of your platform&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Application fees
&lt;/h2&gt;

&lt;p&gt;The first solution for taking a cut is to use application fees with &lt;strong&gt;direct&lt;/strong&gt; or &lt;strong&gt;destination&lt;/strong&gt; charges. Soon after the direct charges funds flow was released, Stripe added support for “fee splitting” with the &lt;a href="https://stripe.com/docs/api/payment_intents/create#create_payment_intent-application_fee_amount" rel="noopener noreferrer"&gt;&lt;code&gt;application_fee_amount&lt;/code&gt;&lt;/a&gt; parameter. The &lt;code&gt;application_fee_amount&lt;/code&gt; parameter can be included in an API call to create payment-related objects like a PaymentIntent or Checkout Session. Application fees are first class objects in the API and represent funds going from the connected account to the platform.&lt;/p&gt;

&lt;p&gt;You can use the API to query for &lt;a href="https://stripe.com/docs/api/application_fees/list" rel="noopener noreferrer"&gt;Application fees&lt;/a&gt;. Or you can view them from your platform dashboard –  you’ll see the fees collected under &lt;a href="https://dashboard.stripe.com/test/connect/application_fees" rel="noopener noreferrer"&gt;Payments &amp;gt; All Payments &amp;gt; Collected fees&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntlk21wbe129y65gwyts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntlk21wbe129y65gwyts.png" alt="Screenshot of the platform dashboard showing fees collected" width="800" height="674"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your users who have access to the Standard dashboard can see the breakdown of Stripe fees and application fees in their dashboard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3tgxzckgq45j1hd5fber.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3tgxzckgq45j1hd5fber.png" alt="Screenshot of the connected account's dashboard and the fees they see" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct charges with application fees
&lt;/h3&gt;

&lt;p&gt;Here’s an example of adding an application fee of $3.21 to a direct charge for $40 to the connected account with ID &lt;code&gt;acct_1LuN62ClUWl50Go4&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;application_fee_amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"321"&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;"Stripe-Account: acct_1LuN62ClUWl50Go4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as the direct charge is complete, application and stripe fees are collected from the amount and the connected account’s pending balance increases by the net amount:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F03-direct-charge-2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F03-direct-charge-2.png" alt="Diagram of the direct funds flow" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Destination charges with application fees
&lt;/h3&gt;

&lt;p&gt;Here’s an example of adding an application fee of $3.21 to a destination charge for $40 to the connected account with ID &lt;code&gt;acct_1KPRf02R4eodYxfv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;application_fee_amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"321"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"transfer_data[destination]=acct_1KPRf02R4eodYxfv"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recall that a destination charge combines two actions: collecting payment on the platform account, then creating a transfer that moves money to the connected account. Here’s what the flow looks like with application fees:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F04-destination-charge-app-fee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F04-destination-charge-app-fee.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the platform account’s dashboard, you’ll find the collected fees in the same place as those collected with direct charges, here’s what it looks like in this scenario:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F05-platform-collected-fees-destination.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F05-platform-collected-fees-destination.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your user has access to the Express dashboard, this is how they would see the fee when they log-in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F06-express-dashboard-fees.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F06-express-dashboard-fees.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate charge and transfer application fees
&lt;/h3&gt;

&lt;p&gt;Application fees are &lt;strong&gt;not&lt;/strong&gt; supported for the separate charge and transfer funds flow. Instead, you’ll need to use one of the other approaches that we’ll outline next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasons to consider using application fees to take a cut:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You use direct charges&lt;/li&gt;
&lt;li&gt;Your users need to understand the amount the platform account collects and see that as a separate fee&lt;/li&gt;
&lt;li&gt;You need to query the API for the list of fees you’ve collected&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Transfer a smaller amount
&lt;/h2&gt;

&lt;p&gt;The second solution for taking a cut is to transfer an amount less than the total payment to the connected account. You can use this with destination charges or separate charges and transfers. This approach is less explicit about the fee being collected from the platform. No objects are explicitly created representing the amount you, the platform, are collecting. Furthermore, connected accounts will only see the amount transferred into their account without any breakdown of application fees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Destination charge with smaller transfer
&lt;/h3&gt;

&lt;p&gt;Here's an example of collecting a destination charge for $40 and then transferring $35 to the connected account with ID &lt;code&gt;acct_1KPRf02R4eodYxfv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"transfer_data[amount]=3500"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"transfer_data[destination]=acct_1KPRf02R4eodYxfv"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that instead of passing the &lt;code&gt;application_fee_amount&lt;/code&gt;, we include a new &lt;code&gt;transfer_data[amount]&lt;/code&gt; parameter with the amount we want transferred to the destination connected account.&lt;/p&gt;

&lt;p&gt;From your platform dashboard, you’ll see links to the relevant transfer, the amount transferred, and the destination connected account where the transfer was sent:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F07-platform-sct-payment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F07-platform-sct-payment.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The connected account will see that they received a payment of $35 and they don’t see any fees. Original charge amount is not shared, and this may make reporting more difficult as it does not create an application fee object.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F08-express-dashboard-sct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F08-express-dashboard-sct.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F09-destination-charge-smaller-transfer.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F09-destination-charge-smaller-transfer.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate charge and transfer smaller amount
&lt;/h3&gt;

&lt;p&gt;When using separate charges and transfers, you make the API call to create the payment related object, and the API call to create the transfer, so you can transfer less than the payment total. Since the most common use-case for SCT is to split payments across several connected accounts, for this example we’ll use a food delivery service example where we’ll collect a $40 payment, then send $10 to the driver’s connected account and $23 to the restaurant’s connected account.&lt;/p&gt;

&lt;p&gt;First, we’ll collect a $40 payment from the end customer to our platform using a &lt;code&gt;transfer_group&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payment from the platform dashboard perspective appears as any payment collected outside of the context of Connect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F10-plat-sct-payment.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F10-plat-sct-payment.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, using the ID of that payment intent’s latest charge as the source transaction, we’ll create a transfer for $10 to the driver’s connected account (&lt;code&gt;acct_1KPRf02R4eodYxfv&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acct_1KPRf02R4eodYxfv &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-04 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;source_transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ch_3MRIjjCZ6qsJgndJ0s4F8WUP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F11-express-sct-payment-driver.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F11-express-sct-payment-driver.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we’ll send a second transfer of $23 to the restaurant’s connected account &lt;code&gt;acct_1KP8JM2ROgShXwm9&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acct_1KP8JM2ROgShXwm9 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-04 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;source_transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ch_3MRIjjCZ6qsJgndJ0s4F8WUP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F12-express-sct-payment-restaurant.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F12-express-sct-payment-restaurant.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again from the platform dashboard, we can see the transfer:&lt;/p&gt;

&lt;p&gt;Here’s a diagram of the food delivery use-case. Again, the end customer pays $40, then the driver receives $10 and the restaurant receives $23. After paying the Stripe fees, the platform keeps $5.54.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F13-sct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcjav.dev%2Fconnect%2F13-sct.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To understand what was included in the transfer, you’ll need to look under &lt;a href="https://dashboard.stripe.com/test/connect/transfers" rel="noopener noreferrer"&gt;Balances &amp;gt; Transfers&lt;/a&gt; then drill into the specific transfer which has links to the source transaction and the payment created on the destination connected account.  \&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasons to consider transferring a smaller amount:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to make transfers to several connected accounts&lt;/li&gt;
&lt;li&gt;You don’t want your users to see the application fee amounts or to know how much the original charge was for&lt;/li&gt;
&lt;li&gt;You do not need to query for the list of fees collected and will build your own reporting solution&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Collect a one-time payment using account debits
&lt;/h2&gt;

&lt;p&gt;There are a handful of use-cases where you might need to move money one-time from the connected account to your platform. You might need to recover funds for a previous refund, correct an error on the connected account’s balance, or as a one-time fee for your products or services. With Connect, your &lt;a href="https://stripe.com/docs/connect/account-debits#transferring-from-a-connected-account" rel="noopener noreferrer"&gt;platform can debit&lt;/a&gt; the Stripe balance of an Express or Custom account using one of two methods: charge &lt;em&gt;or&lt;/em&gt; transfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collect a one-time payment from a connected account with a Charge
&lt;/h3&gt;

&lt;p&gt;Using the lower-level Charges API (not PaymentIntents), you can use the connected account as the source and create a charge that will collect payment from the connected account to your platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/charges &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_1234: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1500 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"acct_1LuN62ClUWl50Go4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Collect a one-time payment from a connected account with a Transfer
&lt;/h3&gt;

&lt;p&gt;Recall that a Transfer moves money from one Stripe account to another. Most of the time, we use Transfers indirectly with destination charges, or directly with separate charge and transfer to move money from the platform to the connected account. In this scenario, we’ll create a Transfer to move money &lt;em&gt;from&lt;/em&gt; the connected account back to the platform.&lt;/p&gt;

&lt;p&gt;This time, we need to use the Stripe-Account header set to the ID of the connected account. This means we’re creating the Transfer &lt;em&gt;on&lt;/em&gt; the connected account (&lt;code&gt;acct_1LuN62ClUWl50Go4&lt;/code&gt;) and the destination is the platform (&lt;code&gt;acct_1EceeUCZ6qsJgndJ&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_4eC39HqLyjWDarjtT1zdp7dc: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Stripe-Account: acct_1LuN62ClUWl50Go4"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1500 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"currency"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"usd"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"destination"&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"acct_1EceeUCZ6qsJgndJ"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that account debits only work for Express and Custom account types and there are several other restrictions you’ll need to review in the &lt;a href="https://site-admin.stripe.com/docs/connect/account-debits#transferring-from-a-connected-account" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasons to consider using account debits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-off payments from the connected account to the platform&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Collect recurring payments from your users as customers
&lt;/h2&gt;

&lt;p&gt;While the title of the article references taking a cut or commission on payments you facilitate, you might want to monetize your platform without commissions.  Let's say you want to collect $10 USD each month from your users, owners of the connected accounts with Stripe Billing. This is not possible if you only have the connected account. You’ll need to collect payment details, create a Customer, and create a Subscription instead.&lt;/p&gt;

&lt;p&gt;When integrating with Stripe's Billing APIs for recurring payments, it's important to keep in mind that creating a Subscription requires having a Customer object, and you cannot use the connected account for this. A Customer and an Account are completely separate objects in Stripe's API, and there is no way to turn one into the other or "share" information between them.&lt;/p&gt;

&lt;p&gt;The most common approach for platforms using Billing is to create a separate Customer object and handle all the Billing logic like payment method updates, handling payments, and proration using Subscriptions. To mitigate the confusion between the Account and the Customer objects for your users, you can use metadata on both the Customer and Account to track the association.&lt;/p&gt;

&lt;p&gt;If you’re interested in taking this approach, take a look at the &lt;a href="https://dev.to/cjav_dev/series/19251"&gt;SaaS fundamentals&lt;/a&gt; series to get started.&lt;/p&gt;

&lt;p&gt;Consider collecting payment from your users as customers if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to collect $N each month&lt;/li&gt;
&lt;li&gt;You do not want to collect a small part of each transaction&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Your approach to taking a cut from payments your platform facilitates will depend on a number of factors. Here’s a summary of my recommendations:&lt;/p&gt;

&lt;p&gt;If you want your users to see the fees collected, use application fees with the &lt;code&gt;application_fee_amount&lt;/code&gt; parameter for direct or destination charges.&lt;/p&gt;

&lt;p&gt;If you want to obfuscate the charge or fee totals, use transfers with amounts less than the payment total.&lt;/p&gt;

&lt;p&gt;If you want to charge a fixed monthly fee, or a usage based amount every month, create platform customers for each of your users and charge them with a Stripe Billing subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" alt="CJ Avilla" width="229" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cjav.dev" rel="noopener noreferrer"&gt;CJ Avilla&lt;/a&gt; (&lt;a href="https://twitter.com/cjav_dev" rel="noopener noreferrer"&gt;@cjav_dev&lt;/a&gt;) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a &lt;a href="https://www.youtube.com/CJAvilla" rel="noopener noreferrer"&gt;YouTuber&lt;/a&gt;. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲. &lt;/p&gt;

</description>
      <category>gratitude</category>
    </item>
    <item>
      <title>Picking the right charge type for your Stripe Connect platform</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Mon, 23 Jan 2023 16:23:15 +0000</pubDate>
      <link>https://dev.to/4thzoa/picking-the-right-charge-type-for-your-stripe-connect-platform-3m14</link>
      <guid>https://dev.to/4thzoa/picking-the-right-charge-type-for-your-stripe-connect-platform-3m14</guid>
      <description>&lt;p&gt;Stripe Connect is a set of APIs for routing payments between multiple parties. Connect can collect payments from end customers and send that money to connected accounts. There are several options for moving money, sometimes called funds flows or charge types. In this article, we'll explore the different options: Direct, Destination, and Separate Charges and Transfers (SCT).&lt;/p&gt;

&lt;p&gt;Choosing the correct charge type for your integration can significantly impact user experience, responsibility for chargebacks and risk management, and reporting. We'll compare and contrast the different flows, highlighting their key features, use cases, and potential pitfalls. We'll also cover which connected account configurations work best with each funds flow. Whether you're a seasoned developer or new to Stripe Connect, this guide will give you the information you need to decide which suits you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;We'll use the term "platform" or "platform account" to refer to the Stripe account you're using to build your platform or marketplace. The "connected account" is the Stripe Account of your user, the person or the business you are enabling. When a payment is collected from an end customer, that payment can only "settle" on one account, which is sometimes referred to as the merchant of record or the settlement account. The settlement account's details are the ones that appear on the end customer's statement. Money can also move between Stripe accounts via either Transfers or Application Fees. These mechanisms allow you to split payments between one or many recipients and to collect fees during the transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct charges
&lt;/h2&gt;

&lt;p&gt;Direct charges were introduced as the first Connect funds flow, and it's the easiest to reason about. Direct charges allow platforms to collect payments from end customers, with the payment going directly to a particular connected account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Characteristics of direct charges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored on the connected account - we say the connected account is the settlement merchant.&lt;/li&gt;
&lt;li&gt;The connected account pays Stripe processing fees.&lt;/li&gt;
&lt;li&gt;The connected account is responsible for handling disputes and managing refunds. The platform can help, but the end responsibility lies on the connected account.&lt;/li&gt;
&lt;li&gt;Most compatible with accounts with access to the Standard dashboard.&lt;/li&gt;
&lt;li&gt;Customers directly transact with your user, often unaware of your platform's existence.&lt;/li&gt;
&lt;li&gt;A single connected account (your user) is involved in the transaction.&lt;/li&gt;
&lt;li&gt;You, the platform, may be insulated from fraud and disputes, depending on who is responsible for losses on the connected account. For Standard, Stripe manages fraud and losses on the connected account.&lt;/li&gt;
&lt;li&gt;Suitable for platforms that are infrastructure providers for other businesses (ex. Shopify or Squarespace)&lt;/li&gt;
&lt;li&gt;Only supported for connect accounts with the card_payments capability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Direct charges are made by adding a Stripe-Account header to an API call when creating payment-related objects like PaymentIntents or Checkout Sessions. For example, one of the connected accounts for my platform has ID &lt;code&gt;acct_1LuN62ClUWl50Go4&lt;/code&gt;. Here's an example API call to create a PaymentIntent for $20 USD using a direct charge that will flow into the connected account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&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;"Stripe-Account: acct_1LuN62ClUWl50Go4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because direct charges are distributed across connected accounts on your&lt;br&gt;
platform, it can be challenging to query for and report on aggregate statistics&lt;br&gt;
related to direct charges. As a platform owner, you have to go to each&lt;br&gt;
connected account and see the direct charges made on that account.&lt;/p&gt;

&lt;p&gt;There are several reasons why you should avoid using Direct charges with&lt;br&gt;
Express or Custom account types:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Addressing disputes becomes increasingly tricky through the dashboard.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Express dashboard does not support dispute management, so you, as the&lt;br&gt;
platform, will need to handle disputes for Express accounts or build dispute&lt;br&gt;
management in your own platform dashboard. This involves building complex state&lt;br&gt;
management to collect evidence and submit it.&lt;/p&gt;

&lt;p&gt;When you're starting out and only have a couple connected accounts, it's&lt;br&gt;
relatively easy to check each account for the necessary charge and then address&lt;br&gt;
a dispute. However, finding the correct charge and dispute becomes more&lt;br&gt;
challenging as your business scales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You, as the platform, have to cover negative balances of your Express and Custom accounts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Refunds and disputes for Direct Charges come from the connected account's balance, and Stripe fees (which are paid by the connected account for Direct Charges) are not returned, making it more likely for a connected account to have a negative balance. You are responsible for your Express and Custom accounts' negative balances, so Stripe has to hold a reserve from your available balance to cover the negative balances across your connected accounts. You can read more about reserve balances in the documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct charges require more work to manage Radar Rules across all connected accounts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your platform account Radar rules only apply to your charges, not charges&lt;br&gt;
directly created on connected accounts. This is a problem because Direct&lt;br&gt;
Charges use the connected account's rules, and Express and Custom accounts&lt;br&gt;
cannot set their own Radar rules, so you, as the platform, would need to&lt;br&gt;
configure the rules for every connected account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example use-cases for Direct charges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An e-commerce platform like Shopify or Squarespace&lt;/li&gt;
&lt;li&gt;An accounting platform that enables invoice payments like Freshbooks&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Destination charges
&lt;/h2&gt;

&lt;p&gt;The second Stripe Connect funds flow we'll discuss is &lt;strong&gt;destination charges&lt;/strong&gt;. Destination charges allow platforms to create a charge and a transfer object with one API call. Instead of using the Stripe-Account header, you make an API call to create a payment-related object with the &lt;code&gt;transfer_data.destination&lt;/code&gt; set to the connected account's ID. This creates an atomic transaction, where the charge and transfer are made at the same time.&lt;/p&gt;

&lt;p&gt;This one API call will create the payment on the platform, then create a transfer moving $20 USD to the destination account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"transfer_data[destination]=acct_1LuN62ClUWl50Go4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Characteristics of destination charges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The charge is on the platform account&lt;/li&gt;
&lt;li&gt;Best funds flow for Express and Custom account types&lt;/li&gt;
&lt;li&gt;Customers transact with your platform for products or services provided by your user&lt;/li&gt;
&lt;li&gt;A single connected account is involved in the transaction&lt;/li&gt;
&lt;li&gt;The platform can reverse the transfer independently of refunds/disputes on the charge&lt;/li&gt;
&lt;li&gt;The platform has more control but is more involved in the flow of funds&lt;/li&gt;
&lt;li&gt;Suitable for platforms that are a unified business&lt;/li&gt;
&lt;li&gt;Uses the Stripe payment fee pricing of the platform&lt;/li&gt;
&lt;li&gt;Allows the platform to share customers and their payment methods across connected accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example use-cases for destination charges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A ride-hailing service like Lyft&lt;/li&gt;
&lt;li&gt;A services platform like Thumbtack&lt;/li&gt;
&lt;li&gt;Platforms for creators&lt;/li&gt;
&lt;li&gt;Tipping and affiliate marketing programs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, you, the platform, still pay the Stripe processing fees. Still, the entire amount was sent to the connected account, so your platform is "out" the processing fees in this example. Stay tuned for the next article, where we'll talk about application fees and approaches to splitting the payment between your platform and the connected account so that you can take a cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate Charges and Transfers (SCT)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Separate charges and transfers&lt;/strong&gt; were introduced to collect payment and transfer proceeds at different times. This funds flow is only supported if both your platform and the connected account are in the same region. For example, both in Europe or both in the U.S.&lt;/p&gt;

&lt;p&gt;Using separate charges and transfers requires at least 2 API calls. One to create the payment-related object like a PaymentIntent or Checkout Session, then a second to create a Transfer to move money from the platform to one or more connected accounts. You can even create several Transfers to split a payment between several connected accounts.&lt;/p&gt;

&lt;p&gt;First, we'll collect payment from the end customer to our platform (note that this does &lt;em&gt;not&lt;/em&gt; use the Stripe-Account header):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we'll transfer part or all of those funds to the connected account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acct_1LuN62ClUWl50Go4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(If you're curious about the &lt;code&gt;transfer_group&lt;/code&gt; parameter, stay tuned!)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Characteristics of SCT:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The charge is stored on the platform&lt;/li&gt;
&lt;li&gt;The platform pays the Stripe processing fees&lt;/li&gt;
&lt;li&gt;Express or Custom account types work best because it gives the platform more flexibility, for instance, when it comes to handling fraud and disputes&lt;/li&gt;
&lt;li&gt;It should only be considered after ruling out destination charges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SCT is ideal in any of these instances:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple connected accounts are involved in the transaction&lt;/li&gt;
&lt;li&gt;A specific connected account isn't known at the time of charge&lt;/li&gt;
&lt;li&gt;Transfer can't be made at the time of charge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example use-cases for SCT:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An e-commerce marketplace that allows a single shopping cart for goods sold by multiple businesses&lt;/li&gt;
&lt;li&gt;A delivery service, like Instacart, that needs to split a payment between a store (the source of the items being delivered) and a delivery person&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transfer group
&lt;/h3&gt;

&lt;p&gt;Think carefully about the 2 API calls we just made. If we leave out the &lt;code&gt;transfer_group&lt;/code&gt; parameter, there is no way to know which payment is related to the transfer. Another issue you'll encounter with those API calls is related to pending funds. Depending on the payment method used, funds could take a while to "settle" into an account. By default, you can only transfer funds that have been settled. So enter two new API parameters that we can use to address these issues: &lt;a href="https://stripe.com/docs/api/payment_intents/create#create_payment_intent-transfer_group" rel="noopener noreferrer"&gt;&lt;code&gt;transfer_group&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://stripe.com/docs/api/transfers/create?lang=curl#create_transfer-source_transaction" rel="noopener noreferrer"&gt;&lt;code&gt;source_transaction&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;transfer_group&lt;/code&gt; parameter is used to associate charges with transfers. It is primarily used for reporting purposes, allowing you to query all transactions by transfer group through the API.&lt;/p&gt;

&lt;p&gt;Let's update our API calls to include a transfer group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/payment_intents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;payment_method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pm_card_visa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acct_1LuN62ClUWl50Go4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Note that for transfer groups, you must be configured to use manual payouts or a combination of automatic payouts and the &lt;code&gt;source_transaction&lt;/code&gt; parameter to ensure the platform has sufficient funds to cover the transfer.&lt;/p&gt;

&lt;p&gt;You can group multiple charges with a single transfer or multiple transfers with a single charge. You can also perform transfers and charges in any order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Source transaction
&lt;/h3&gt;

&lt;p&gt;The source transaction parameter helps transfer pending funds, allowing you, the platform, to make a PaymentIntent and immediately create a transfer, even if the charge is still pending.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can use this parameter to transfer funds from a charge before they are&lt;br&gt;
added to your available balance. A pending balance will transfer immediately&lt;br&gt;
but the funds will not become available until the original charge becomes&lt;br&gt;
available. See the &lt;a href="https://stripe.com/docs/connect/charges-transfers#transfer-availability" rel="noopener noreferrer"&gt;Connect documentation for&lt;br&gt;
details&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.stripe.com/v1/transfers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; sk_test_abc123: &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;usd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acct_1LuN62ClUWl50Go4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;transfer_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo-01 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;source_transaction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ch_3MPoP2CZ6qsJgndJ1u1Uhi01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you'll need to pass the ID of the underlying Charge, not the ID of the PaymentIntent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Stripe Connect provides a range of funds flows that you can use to route payments between multiple parties. Each has different use cases, advantages, and drawbacks. Direct charges are the easiest to implement but are limited to Standard account types and have difficulty with dispute management and reporting. Destination charges are ideal for Express and Custom account types and provide more control. Finally, separate Charges and Transfers (SCT) require at least two API calls and are best used when multiple connected accounts are involved in the transaction. I recommend weighing the advantages and disadvantages of each funds flow and deciding which is best for your integration. Also, be sure to account for reserves and use &lt;code&gt;transfer_group&lt;/code&gt; and &lt;code&gt;source_transaction&lt;/code&gt; where applicable.&lt;/p&gt;

&lt;p&gt;The charge-type decision comes down to a few factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who does the end customer think they are doing business with? I.e., who's the settlement merchant that will appear on the statement.

&lt;ul&gt;
&lt;li&gt;As a Lyft passenger, I expect to be doing business with Lyft and not the driver. (SCT or Destination)&lt;/li&gt;
&lt;li&gt;As a customer buying shoes from a Shopify store, I expect to be doing business with the store. (Direct)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Will the payment need to be split among multiple recipient connected accounts? (SCT)&lt;/li&gt;

&lt;li&gt;Who should pay the Stripe fees?&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" alt="CJ Avilla" width="229" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cjav.dev" rel="noopener noreferrer"&gt;CJ Avilla&lt;/a&gt; (&lt;a href="https://twitter.com/cjav_dev" rel="noopener noreferrer"&gt;@cjav_dev&lt;/a&gt;) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a &lt;a href="https://www.youtube.com/CJAvilla" rel="noopener noreferrer"&gt;YouTuber&lt;/a&gt;. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲. &lt;/p&gt;

</description>
      <category>stripe</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Standard vs. Express vs. Custom account types for Stripe Connect</title>
      <dc:creator>CJ Avilla</dc:creator>
      <pubDate>Fri, 06 Jan 2023 21:50:12 +0000</pubDate>
      <link>https://dev.to/4thzoa/standard-vs-express-vs-custom-account-types-for-stripe-connect-386j</link>
      <guid>https://dev.to/4thzoa/standard-vs-express-vs-custom-account-types-for-stripe-connect-386j</guid>
      <description>&lt;p&gt;In this series, you’ll learn all about Stripe Connect, the set of Stripe APIs and tools for building a platform or marketplace to route payments between multiple parties.&lt;/p&gt;

&lt;p&gt;If you’re using Stripe Connect, you’ll need to create an account (called a "connected account") for each user who collects payments with (or accepts payouts from) your platform. You’ll do this every time a user signs up for your platform. The &lt;a href="https://stripe.com/docs/connect/accounts" rel="noopener noreferrer"&gt;type of account&lt;/a&gt; you choose for your user will determine the development work required for the Stripe integration you need to build (options range from Stripe-hosted to completely custom) and what operational responsibilities you’ll have (like handling chargebacks and supporting your users). It’s possible to mix and match account types for your platform if you are solving for different use-cases or segments. There are three account types to choose from with Connect, each designed for different needs: Standard, Express, and Custom. We’ll first compare and contrast the integration effort and operational overhead, then close with some recommendations.&lt;/p&gt;

&lt;p&gt;💰 Keep in mind, there is an additional cost for using Express or Custom account types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration effort
&lt;/h2&gt;

&lt;p&gt;When it comes to integration effort, Standard and Express accounts are the most straightforward options with Standard accounts requiring the least amount of work to integrate. However, if you want more control over the user experience, you may want to consider Custom accounts. While Custom accounts allow you to whitelabel the entire experience, they require &lt;strong&gt;significantly more&lt;/strong&gt; development effort to implement.&lt;/p&gt;

&lt;p&gt;Here are more details about the integration requirements for each account type:&lt;/p&gt;

&lt;h3&gt;
  
  
  Onboarding
&lt;/h3&gt;

&lt;p&gt;Stripe-hosted onboarding supports all account types. The hosted onboarding integration requires at least two API calls.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://stripe.com/docs/api/accounts/create" rel="noopener noreferrer"&gt;Create a connected Account&lt;/a&gt;. Note that you can pre-populate many account details if you like the user’s website, support phone number, mailing address, etc. that way, they don’t need to re-enter those details when onboarding.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="s1"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://stripe.com/docs/api/account_links/create" rel="noopener noreferrer"&gt;Create an Account Link&lt;/a&gt; that redirects the user through the Stripe hosted onboarding flow:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accountLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountLinks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&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;refresh_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/reauth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/return&lt;/span&gt;&lt;span class="dl"&gt;'&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="s1"&gt;account_onboarding&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="c1"&gt;// redirect to accountLink.url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://stripe.com/connect/onboarding" rel="noopener noreferrer"&gt;Connect Onboarding&lt;/a&gt; makes it easy to onboard merchants to your platform by providing conversion-optimized, pre-built user interfaces. This means you can easily get your merchants set up without having to worry about the complexity of building your own identity verification and onboarding flows. Plus, Connect Onboarding helps you minimize compliance and operational issues.&lt;/p&gt;

&lt;p&gt;I recommend using Connect Onboarding for all account types. It’s worth calling out that you can technically build your own custom onboarding flow for Custom accounts and use the API to update details about the account and check for a list of additional verification requirements. Localizing a custom form and nailing all of the identity verification flows for multiple countries is quite a heavy lift so unless you have significant engineering resources, I would default to using Connect Onboarding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic updates for new compliance requirements
&lt;/h3&gt;

&lt;p&gt;Global compliance requirements and rules do change over time. For Standard and Express accounts, Stripe will proactively reach out and collect information about your accounts whenever requirements change.&lt;/p&gt;

&lt;p&gt;If you’re using Custom account types with a bespoke onboarding flow and haven’t integrated with Connect Onboarding, you’ll need to invest development resources to make updates as compliance requirements change over time. However, if you use Connect Onboarding for Custom accounts, you can easily collect onboarding and verification information from your users and save yourself the hassle of constantly updating your onboarding form. Plus, Connect Onboarding can help you stay up-to-date with the latest compliance requirements without any extra effort on your part. Just make sure to follow best practices for communicating changes to your users, as outlined in the guide for Custom accounts.&lt;/p&gt;

&lt;p&gt;Integration effort is one the most heavily weighted factors when deciding which Stripe Connect account type to choose. Most startup and growth companies have the bandwidth and resources to implement and maintain Standard and Express accounts. If you don’t have a team of engineers to dedicate significant bandwidth to maintaining a Custom integration, I recommend Standard or Express.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational responsibilities
&lt;/h2&gt;

&lt;p&gt;Another important consideration is who handles the operational responsibility day to day. For instance, are you the platform liable for fraud and disputes related to payments for a connected account? What type of dashboard can the connected account access to view payments, issue refunds, handle disputes, and update their information? Who is responsible for supporting connected account users when questions arise? Some of the following details differ depending on the charge type (direct vs destination) which we’ll cover in another article. Just know that one charge type may have different operational requirements than another charge type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fraud and dispute liability
&lt;/h3&gt;

&lt;p&gt;If you’re trying to decide between Standard and Express accounts, this is likely the tie breaker.&lt;/p&gt;

&lt;p&gt;For Standard accounts, the connected account owner (your user) is responsible for dealing with fraud and disputes*. For Express and Custom accounts, you as the platform are ultimately responsible for dealing with fraud and disputes.&lt;/p&gt;

&lt;p&gt;Understanding the dispute process and how to handle fraud is part of any online business. Standard accounts are more suited for experienced online businesses, whereas Express accounts are ideal for businesses at any level of experience.&lt;/p&gt;

&lt;p&gt;*this may shift depending on the charge type used.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard access
&lt;/h3&gt;

&lt;p&gt;Standard accounts are conventional Stripe accounts where the account holder (your user) is able to log in to the Dashboard and can process charges on their own. If you want to obfuscate the relationship between the connected account holder and their end user for any reason, Standard is likely not a good fit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1v1s4erk7nntp3q1gsu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw1v1s4erk7nntp3q1gsu.png" alt="Screenshot of Standard Dashboard" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Express accounts can only use the Express Dashboard, a simplified Stripe-hosted interface. From the Express Dashboard, your users can view their available balance, see upcoming payouts, and track their earnings in real time. The Express Dashboard does not have all of the same features as the Standard Dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dkajjto20x74nuh6eab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dkajjto20x74nuh6eab.png" alt="Screenshot of Express Dashboard" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Custom accounts do not have access to a Stripe-hosted dashboard. You’ll need to build custom interfaces into your application for your users to handle any operational tasks related to running their business.&lt;/p&gt;

&lt;h3&gt;
  
  
  User support
&lt;/h3&gt;

&lt;p&gt;All support questions about a Custom connected account are handled by you, the platform. For Standard and Express accounts, some questions can be answered by Stripe and others will be handled by the platform. For instance, as identity verification requirements shift and change, Stripe will proactively reach out to your users to ensure the information on file stays compliant with local regulations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations
&lt;/h2&gt;

&lt;p&gt;Absent a compelling reason for a bespoke integration and lots of resources to build and maintain a Custom Connect integration, I would default to either Standard or Express. Assuming the platform I’m building is open to handling fraud and disputes, the platform decision between Standard and Express comes down to the relationship I expect users to have with end customers. Will my users, the owners of the connected accounts, need a long standing relationship with their customers or will their relationship be one-off, and transactional in nature? If so, do I expect my customer base to be savvy enough (or teachable enough) to handle fraud and disputes and trusted with the ability to charge the end customer manually. If I’m working with online business savvy folks who maintain long-running relationships with their end customers, I’d choose Standard. If I’m working with any internet user who has a one-off relationship with end customers or if I want to prevent them from charging end customers, then I’d lean towards Express.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8t0gbpesgerk30xxce6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8t0gbpesgerk30xxce6.png" alt="Stripe Connect account type decision tree" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmef4k8k1jhnula9b1mxc.png" alt="CJ Avilla" width="229" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cjav.dev" rel="noopener noreferrer"&gt;CJ Avilla&lt;/a&gt; (&lt;a href="https://twitter.com/cjav_dev" rel="noopener noreferrer"&gt;@cjav_dev&lt;/a&gt;) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a &lt;a href="https://www.youtube.com/CJAvilla" rel="noopener noreferrer"&gt;YouTuber&lt;/a&gt;. He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲. &lt;/p&gt;

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