
API Advice
In Depth: Speakeasy vs Stainless

Nolan Sullivan
January 10, 2025

NOTE
This comparison of Speakeasy & Stainless is based on a snapshot of two developing companies as of January 2025. If you think we need to update this post, please let us know!
In this post, we’ll compare generated SDKs, as well as the underlying philosophies that guide the development of the two companies. And while we acknowledge that our views may be biased, we’ll show our work along with our conclusions so that readers can decide for themselves.
In short: How is Speakeasy different?
OpenAPI-native vs OpenAPI-compatible
Speakeasy is OpenAPI-native; Stainless is OpenAPI-compatible. Stainless is built on a custom DSL known as the Stainless config (opens in a new tab). This approach requires your team to manage an additional config file. Speakeasy has no intermediary DSL. Your OpenAPI spec is the only source of truth for SDK generation.
Being OpenAPI-native is beneficial for integration into an existing stack. Regardless of the API proxies, server-side code, or specific web framework that you’re using, you can plug in Speakeasy’s tools. Stainless is doubling down on a vertically integrated approach by building a backend TypeScript framework (opens in a new tab) which will become the foundation for their solution. Their product will work best when you buy the entire stack, Speakeasy will shine regardless of the other tools in your existing stack.
Type-safe vs type-faith
There’s more on this topic in the technical deep dive below. Speakeasy SDKs guarantee true end-to-end type safety, meaning that types are generated to validate both request and response objects defined in your API. Stainless SDKs, on the other hand, are mainly type-hinted, not guarding the API from bad inputs.
Velocity and maturity
Stainless was founded in 2021 (opens in a new tab) and has expanded its language support to seven languages. By comparison, Speakeasy launched in October 2022 and has released support for ten languages in less time. The Speakeasy platform is also broader, with support for additional generation features, like webhooks, React Hooks, and contract testing, not supported by Stainless.
Both companies are financially secure, having raised $25M+ in funding.
Platform
Product | Speakeasy | Stainless |
---|---|---|
SDK generation | ✅ | ✅ |
Terraform generation | ✅ | ✅ |
Docs generation | ✅ | ❌ |
Contract testing | ✅ | ❌ |
E2E testing | ✅ | ❌ |
SDK generation
Language | Speakeasy | Stainless |
---|---|---|
TypeScript | ✅ | ✅ |
Python | ✅ | ✅ |
Go | ✅ | ✅ |
Java | ✅ | ✅ |
Kotlin | ⚠ Java is Kotlin-compatible | ✅ |
Ruby | ✅ | ✅ |
C# | ✅ | ❌ |
PHP | ✅ | ❌ |
Swift | ✅ | ❌ |
Unity | ✅ | ❌ |
If there’s a language you require that we don’t support, add it to our roadmap (opens in a new tab).
SDK features
Breadth matters, but so does the depth of support for each language. The table below shows the current feature support for Speakeasy and Stainless’s SDK generation.
Feature | Speakeasy | Stainless |
---|---|---|
Webhooks support | ✅ | ❌ |
React Hooks | ✅ | ❌ |
OAuth 2.0 | ✅ | ❌ (manual) |
React Hooks support | ✅ | ❌ (manual) |
Retries | ✅ | ✅ |
Pagination | ✅ | ✅ |
Async functions | ✅ | ✅ |
Streaming uploads | ✅ | ✅ |
Custom SDK naming | ✅ | ✅ |
Union types | ✅ | ✅ |
Discriminated union types | ✅ | ❌ |
Server-sent events | ✅ | ⚠ non-OpenAPI standard |
Custom dependency injection | ✅ | ✅ |
Pricing
In terms of pricing, both Speakeasy and Stainless offer free plans, as well as paid plans for startups and enterprises. The most significant pricing difference is in the enterprise plan. Existing customers indicate that Stainless’s enterprise pricing is ~20% higher than Speakeasy’s. Of course, this can vary, and we recommend reaching out to both companies for a quote.
Plan | Speakeasy | Stainless |
---|---|---|
Free | 1 free Published SDK | 1 free local SDK; max 50 endpoints |
Startup | $250/mo/SDK; max 50 endpoints | $250/mo/SDK; max 50 endpoints |
Business | 600/mo/SDK ; max 200 endpoints | $2,500/mo; max 5 SDKs; 150 endpoints |
Enterprise | Custom | Custom |
Speakeasy vs Stainless: TypeScript SDK comparison
NOTE
For this technical comparison, we’ll create SDKs with Speakeasy and Stainless using an example bookstore API. You can find the complete OpenAPI document in this example repository (opens in a new tab).
SDK structure
Speakeasy maintains a clear separation of concerns. There are separate folders for models and operations, both in the documentation and in the source folder, and distinct files for each component and operation.
Stainless generates an SDK that, at a glance, looks less organized, considering the greater number of configuration files at the root of the project, no separation of models and operations, and more shims scattered throughout.
Structure might seem superficial at first, but keep in mind that SDK users form their first impressions of your SDK from the same high-level overview. Some of this may be a matter of opinion, but at Speakeasy, we aim to generate SDKs that are as organized as SDKs coded by hand.
Generated types and type safety
Both Speakeasy and Stainless generate TypeScript types, enabling developers to see errors and hints during development. However, Stainless does not generate types for complex OpenAPI component schemas.
For example, consider the following Author
component from our OpenAPI document.
Author:type: objectproperties:id:$ref: "#/components/schemas/AuthorId"name:type: stringexample: Robert C. Martinphoto:type: stringexample: https://example.com/photos/robert.jpgbiography:type: stringexample: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...anyOf:- required:- nametitle: Author with name- required:- idtitle: Author with IDexample:id: 1name: Robert C. Martinphoto: https://example.com/photos/robert.jpgbiography: Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...
The highlighted anyOf
list is of particular interest. This list states that a valid Author
object must have a name
or an id
, or both. An author with neither a name
nor an id
should not validate against this schema.
Let’s take a look at the relevant types generated by each SDK generator:
// ...export type AuthorWithID = {id: number;Only id is requiredname?: string | undefined;photo?: string | undefined;biography?: string | undefined;};export type AuthorWithName = {id?: number | undefined;name: string;Only name is requiredphoto?: string | undefined;biography?: string | undefined;};export type Author = AuthorWithName | AuthorWithID;
// ...export namespace FantasyBook {export interface Author {id?: number;Both id and name are optionalbiography?: string;name?: string;Both id and name are optionalphoto?: string;}}// Repeated for ProgrammingBook and SciFiBook
Here, we see that Speakeasy generates three types for the Author
schema: AuthorWithID
, AuthorWithName
, and a union of these, called Author
.
The equivalent type generated by Stainless is an Author
interface for each book type, with both id
and name
marked as optional.
As a result, the following example code using the Stainless SDK will incorrectly compile without any warnings. The equivalent Speakeasy code will correctly catch the compilation error:
// techbooks-speakeasy SDK created by Speakeasyimport { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks({apiKey: "123",});async function run() {await bookStore.books.addBook({author: {},Compilation error: Type '{}' is not assignable to type 'Author'.category: "Programming",description: "A Handbook of Agile Software Craftsmanship",price: 2999,title: "Clean Code",});}run();
// techbooks SDK generated by Stainlessimport { Techbooks } from "techbooks";const bookStore = new Techbooks({apiKey: "My API Key",clientId: "My Client ID",clientSecret: "My Client Secret",});async function run() {const params: Techbooks.BookCreateParams = {author: {},No compilation error, even though the author object is emptycategory: "Programming",description: "A Handbook of Agile Software Craftsmanship",price: 2999,title: "Clean Code",};const result: Techbooks.BookCreateResponse = await bookStore.books.create(params);}run();
Runtime type checking
This brings us to the next type error that should be caught: runtime type errors. Speakeasy creates SDKs that are type-safe from development to production. As our CEO wrote, type safe is better than type faith.
The SDK generated by Speakeasy uses Zod (opens in a new tab) to validate the data sent to and received from the server. This provides safer runtime code execution and helps developers who use your SDK to receive early feedback about the data their app is sending. Furthermore, data validation on the client side gives users more confidence in your API’s reliability by reducing the opportunity for unintended behavior caused by unexpected data.
To see how this works, let’s look at what happens when the price
field (an integer
in the Book
type from our example) is populated with a float
value:
Book:type: objectrequired:- title- description- price- category- authorproperties:id:$ref: "#/components/schemas/ProductId"title:type: stringexample: Clean Codedescription:type: stringexample: A Handbook of Agile Software Craftsmanshipprice:type: integerdescription: Price in USD centsexample: 2999category:type: stringenum:- Sci-fi- Fantasy- Programmingexample: Programming
// techbooks-speakeasy SDK created by Speakeasyimport { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks({apiKey: "123",});async function run() {await bookStore.books.addBook({author: {name: "Robert C. Martin",photo: "https://example.com/photos/robert.jpg",biography: 'Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...',},category: "Programming",description: "A Handbook of Agile Software Craftsmanship",price: 29.99,Validation error: Expected integer, received floattitle: "Clean Code",});}run();
The price
field in the Book
object in our test code is set to 29.99
, which is a floating-point number. This will trigger a Zod validation error before the data is sent to the server, as the price
field is expected to be an integer. Handling Zod validation errors (opens in a new tab) is straightforward, and allows users to get fast feedback on where they are going wrong.
The same book object in code using the Stainless-generated SDK will only be validated on the server. This means that the error will only be caught from the client’s perspective after the data is sent to the server, and the server responds with an error message. If the server is not set up to validate the price
field, the error will not be caught at all, leading to unexpected behavior in your users’ applications.
As a result, developers using the SDK generated by Stainless may need to write additional client-side validation code to catch these errors before they are sent to the server.
OAuth client credentials handling
Both Speakeasy and Stainless generate SDKs that handle OAuth 2.0 with client credentials. However, only Speakeasy’s SDKs handle the token lifecycle, retries, and error handling without any additional code.
Our bookstore API requires an OAuth 2.0 token with client credentials to access the API. Let’s see how the SDKs handle this.
Consider the following OAuth 2.0 configuration from our OpenAPI document:
clientCredentials:type: oauth2flows:clientCredentials:tokenUrl: https://api.bookstore.com/oauth/tokenrefreshUrl: https://api.bookstore.com/oauth/refreshscopes: {}
Speakeasy’s generated SDK takes a clientID
and clientSecret
when instantiating the SDK. The SDK also includes ClientCredentialsHook
class that implements BeforeRequestHook
to check whether the token is expired and refresh it if necessary. The hook also checks whether the client has the necessary scopes to access the endpoint, and handles authentication errors.
import { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks({security: {// OAuth 2.0 client credentialsclientID: "<YOUR_CLIENT_ID_HERE>",clientSecret: "<YOUR_CLIENT_SECRET_HERE>",},});async function run() {// The SDK handles the token lifecycle, retries, and error handling for youawait bookStore.books.addBook({// Book object});}run();
import TechBooks from 'stainless-book-sdk';import axios from 'axios';const clientId = '<YOUR_CLIENT_ID_HERE>';const clientSecret = '<YOUR_CLIENT_SECRET_HERE>';async function getOAuthToken() {const response = await axios.post('https://api.bookstore.com/oauth/token', {grant_type: 'client_credentials',client_id: clientId,client_secret: clientSecret});return response.data.access_token;}const client = new TechBooks({retries: {maxRetries: 3}});async function run() {// Need to manually fetch and manage tokenconst token = await getOAuthToken();client.defaultHeaders = {'Authorization': `Bearer ${token}`,'Content-Type': 'application/json'};await client.books.addBook({// Book object});}run();
The SDK generated by Stainless requires you to manually fetch and manage the OAuth token. This means that you’ll need to write additional code to handle the token lifecycle, retries, and error handling. You’ll also need to include additional configuration in the Stainless configuration file to use Bearer tokens.
client_settings:opts:access_token:type: stringauth:security_scheme: BearerAuthread_env: MY_TEAM_ACCESS_TOKENsecurity:- BearerAuth: []security_schemes:BearerAuth:type: httpscheme: bearer
Speakeasy does not require any additional configuration to handle OAuth 2.0 with client credentials. The SDK itself handles the token lifecycle, retries, and error handling.
Webhooks support
Webhooks enable users to receive real-time updates from your API through HTTP callbacks in your SDK. Speakeasy and Stainless both generate SDKs that support webhooks, but Speakeasy’s SDKs provide built-in support for webhook validation, payload parsing, and delivery.
Stainless doesn’t provide out-of-the-box functionality for handling webhooks. You must implement your own logic for verifying event signatures, such as HMAC or RSA, defining event payload types, and managing retry mechanisms.
The example below shows how the techbooks-speakeasy
SDK can validate and parse a book.created
event, as well as the corresponding OpenAPI spec defining the webhook event.
// techbooks-speakeasy SDK created by Speakeasyimport { TechBooks } from "techbooks-speakeasy";const bookStore = new TechBooks();async function handleWebhook(request: Request) {const secret = "my-webhook-secret";const res = await bookStore.webhooks.validateWebhook({ request, secret });if (res.error) {console.error("Webhook validation failed:", res.error);throw new Error("Invalid webhook signature");}// `res.data` is strongly typed based on your OpenAPI specconst { data, inferredType } = res;switch (data.type) {case "book.created":console.log("New Book Created:", data.title);// ...break;case "book.deleted":console.log("Book Deleted:", data.title);// ...break;default:console.warn(`Unhandled event type: ${inferredType}`);}}
openapi: 3.1.1paths:...x-speakeasy-webhooks:security:type: signature # a preset which signs the request body with HMACname: x-signature # the name of the headerencoding: base64 # encoding of the signature in the headeralgorithm: hmac-sha256webhooks:book.created:post:requestBody:required: truecontent:application/json:schema:type: objectproperties:id:type: stringtitle:type: stringrequired:- id- titleresponses:'200':description: Book creation event receivedbook.deleted:...
You can read more about how Speakeasy handles webhooks in our webhooks release post.
React Hooks
React Hooks simplify state and data management in React apps, enabling developers to consume APIs more efficiently. Speakeasy generates built-in React Hooks using TanStack Query (opens in a new tab). These hooks provide features like intelligent caching, type safety, pagination, and integration with modern React patterns like SSR and Suspense. Stainless does not generate React Hooks.
import { useQuery } from "@tanstack/react-query";function BookShelf() { // loads books from an APIconst { data, status, error } = useQuery(["books" // Cache key for the query], async () => {const response = await fetch("https://api.example.com/books");return response.json();});if (status === "loading") return <p>Loading books...</p>;if (status === "error") return <p>Error: {error?.message}</p>;return (<ul>{data.map((book) => (<li key={book.id}>{book.title}</li>))}</ul>);}
For example, in this basic implementation, the useQuery
hook fetches data from an API endpoint. The cache key ensures unique identification of the query. The status
variable provides the current state of the query: loading
, error
, or success
. Depending on the query status, the component renders loading
, error
, or the fetched data as a list.
For an in-depth look at how Speakeasy uses React Hooks, see our official release article.
Summary
We’ve all experienced bad SDKs that make integration with the API harder, not easier. Speakeasy is building a generator to make poorly written, poorly maintained SDKs a thing of the past. To do so, our team has put an extraordinary level of thought into getting the details of SDK generation right. We think that the effort has earned us the position to compare favorably with any other generator.
If you are interested in seeing how Speakeasy stacks up against some of the popular open-source SDK-generation tools, check out this post.