@alvinsng: https://x.com/alvinsng/status/2077114275412512868
Summary
Alvin Sng explains why their team moved away from using client SDKs for Stripe, WorkOS, and Slack, opting instead to call their REST APIs directly via a centralized wrapper. They argue that SDKs hide critical debugging details, are fragile in production, and encourage anti-patterns that are now more easily avoided with AI-assisted coding.
View Cached Full Text
Cached at: 07/15/26, 03:55 PM
Why we stopped using SDKs
We stopped using the Stripe, WorkOS, and Slack client SDKs and are in the process of migrating the rest away. Instead, we call their REST APIs directly through a small wrapper class we call HttpBaseClient.
That sounds backwards. SDKs are supposed to save you time. But you’ve been hearing about AI porting more code to run natively. The same idea applies to SDKs. Why?
-
SDKs hide the raw details, like HTTP response headers and raw response bodies, which are critical for agents that are debugging issues or sending details to the upstream team for investigation.
-
They expect proper responses, but in reality malformed responses come back from firewalls, load balancers, and gateways. That masks the debugging details we need.
-
SDKs bypass the forcing function of a centralized entry point for retries, error handling, and observability, which lets agents rewrite their own patterns.
-
They carry heavy bloat, usually auto-generated from OpenAPI, when we only use a tiny subset of endpoints.
Think about why SDKs started. They were born because companies wanted adoption to feel easy: ship a shared library, save customers time, and hide the repetitive integration work. But AI changed that cost curve. Integrating with an SDK is now often as much work as calling the HTTP API directly, and it is cheap to write a tailored REST client for exactly the endpoints we use while providing better unified observability.
The endless game of whack-a-mole
Such lovely “JSON” responses
Such lovely “JSON” responses
This is what our Sentry error dashboard used to look like. We would find that a small percent of requests failed from some code path, then we would monkey-patch just that path to handle the SDK mishap for the user. Each day another code path would trip an unexpected error; we would patch that one too, and the game never ended.
SDKs are fragile in a common production failure mode: the server says something went wrong, but the response is not the JSON shape the SDK expected.
An overloaded Nginx gateway returns unexpected HTML. Cloudflare blocks a request. A firewall puts up a rate-limit page. The vendor API is usually JSON, but the thing in front of it is not always the vendor API.
When that happens, many SDKs try to parse the response, fail with a generic parsing error, and throw away the useful bits. The raw body is gone. The status text is buried. The HTTP headers often are not returned in a usable way. If vendor support asks for a request ID from an HTTP header, we’re out of luck because the SDK does not return it.
Agents abuse the directness of SDKs
Our client SDK calls were the wild west. Stripe had one pattern. WorkOS had another. Slack had its own quirks. Other integrations had raw fetch, SDK calls, one-off retry logic, or no retry logic at all.
SDKs made the wrong thing look easy. An agent could always go directly to the vendor client and call stripe.customers.create(...) from any route. That feels productive, but it bypasses the shared place where auth, retries, metrics, logs, and error translation should live. We had closure wrappers like this littered in our codebase:
If you missed a spot and forgot to wrap the SDK call, you had a bad day. A Stripe rate limit and a WorkOS rate limit mean the same thing to our product: the upstream is asking us to slow down. But at the type level, they were totally different objects. Some code caught SDK-specific exceptions. Some caught generic Error. Some caught nothing. That turned into Sentry whack-a-mole: fix one 429 at one call site, wait for the same class of failure somewhere else.
SDKs are bloated
NPM packages openai and anthropic-ai/sdk are auto-generated from OpenAPI specs by Stainless. Stripe is generated from Stripe’s OpenAPI spec too. That is how you scale the maintenance of a public SDK for a large API across dozens of programming languages.
But a great public SDK has to serve everyone. Our backend does not need everyone’s SDK. It needs our eight Stripe endpoints, our WorkOS user and org endpoints, and the Slack methods we actually call. Stripe is 6.5 MB, workos-inc/node is 6.9 MB, slack/web-api is 7.7 MB, and linear/sdk is 34 MB. In the far extreme, googleapis reachs 198 MB.
Generated SDKs bring the whole platform with them: hundreds of methods, overloads, pagination helpers, retry behavior, environment detection, compatibility shims, and old surfaces that cannot disappear because someone, somewhere, still depends on them. Inside our own backend, that generality is usually bloat. Worse, it sits between us and the wire.
We forget HTTP APIs are API contracts
People sometimes talk about HTTP APIs as if they are lower-level implementation details and SDKs are the real stable interface. The public REST API is a contract. Vendors cannot casually break it. SDK authors know this too, because they cannot force every customer to upgrade. Many customers keep running years-old SDK versions in production, which means the old wire contract has to keep working anyway.
Our own HttpBaseClient
HttpBaseClient is our replacement for client SDKs. Provider subclasses supply the vendor-specific pieces: base URL, auth headers, content type, error mapping, and narrow methods for the endpoints we actually use. HttpBaseClient owns the rest: serialization, parsing, transport errors, structured logs, metrics, status mapping, and duration tracking. This unifies observability so every vendor follows consistent standards. Here is the shape, simplified:
That is the trick. The class does not try to model all of Stripe. It models the HTTP behavior we want every vendor call to have. Stripe still gets form encoding. WorkOS still gets bearer auth and JSON bodies. Slack still gets its odd ok: false behavior on HTTP 200. But the rest of our backend sees one consistent shape.
You can view a longer version of what our HttpBaseClient looks like here, modified to be generic and easier to read as example code.
Where we still use SDKs
Our current approach is hybrid: use our own HTTP client at runtime, but keep SDKs around where their types still save time. StripeHttpClient can return Stripe.Customer, SlackHttpClient can borrow slack/web-api argument types, and WorkOS types can still describe the wire response.
I expect us to phase this out over time too. As AI gets better at generating and maintaining the exact request and response types we need, the case for keeping a whole SDK package just for types gets weaker. But runtime behavior is the painful part, so that is where the migration starts.
We still use SDKs when the SDK is the product boundary, not just a wrapper around REST. Observability is the clearest example. For Sentry, the SDK handles runtime instrumentation, error capture, scope propagation, release metadata, and integrations we would not want to hand-roll. That is different from using a vendor SDK as a thin client for ordinary backend HTTP calls.
Not every API is REST over HTTP, and that is fine. Database calls are a good example. Our lower-level abstraction is BaseClient: it gives each client the same metrics, logging, and error-handling contract, while allowing a child class to override what “fetch” means for its transport.
Where we’re headed
Developers will treat API docs as the real integration guide and SDKs as reference implementations: templates for auth, payloads, pagination, retries, and edge cases. The next version of “ship an SDK” may be “ship an agent skill” that teaches agents how to call the API correctly, reuse the right patterns, and avoid pushing every runtime call through a vendor package.
Similar Articles
@rohanpaul_ai: Stripe is seeing a version of Jevons paradox: engineers can build far more in less time, yet the backlog of software wo…
Stripe is experiencing a Jevons paradox where AI enables engineers to build software faster, but the backlog of valuable software continues to grow. Will Gaybrick discusses how AI is changing Stripe's product development process.
@katelyn_lesse: https://x.com/katelyn_lesse/status/2073902681668931927
A Twitter thread by Katelyn Lesse arguing that teams should confront the hardest part of a project head-on rather than avoiding it incrementally, illustrated by the story of Stripe's v2 accounts.
@sashimikun_void: Another day watching agentic Slack startups get wiped. I've heard founders say, "Don't worry, they won't have time to f…
A developer reflects on how AI agents are eliminating Slack startup niches, while ClaudeDevs reveals that Claude Code now writes 65% of their product team's code, including the Claude Tag tool itself.
@garrytan: https://x.com/garrytan/status/2061454423034110372
Garry Tan argues that developers are over-engineering with excessive code when using AI agents; instead, they should trust the model and build minimal, instruction-based software, exemplified by his open-source project GStack.
@RhysSullivan: https://x.com/RhysSullivan/status/2070989582850793947
Rhys Sullivan argues that companies should make their APIs, skills, and knowledge accessible to users' own AI agents rather than forcing everyone to use in-app agents, enabling power users to leverage their preferred models and local context while still offering simple interfaces for regular users.