APIs

Shipping an SDK vs Leaving Callers on Raw HTTP

Every public API makes a promise, and the promise has a shape. A raw HTTP endpoint promises that if a caller sends the right bytes to the right path with the right headers, something useful comes back. An SDK promises something softer and larger: that a person can ask for an outcome in the vocabulary of their own problem and trust the library to handle the rest. The distance between those two promises is where most of the real engineering lives, and it is also where teams tend to underestimate the bill.

Shipping an SDK is a commitment to own a second product. The endpoint is the first product. The client library is the second one, with its own release cadence, its own bug tracker, its own compatibility matrix, and its own users who will open issues at 2 a.m. because a retry policy interacted badly with their proxy. Teams that treat the SDK as a thin wrapper around the HTTP call discover quickly that the wrapper is the part people actually touch.

What raw HTTP actually asks of a caller

Picture a developer integrating a payments or messaging service for the first time. With raw HTTP, the first hour goes to reading the docs, and the second hour goes to discovering that the docs assumed a JSON body shape that differs from the one in the example. By the third hour there is a working curl command. By the end of the day there is a Python script that handles authentication, pagination, and one specific error code that keeps appearing in staging.

None of that work is wrong. It is the honest cost of an unopinionated interface. The caller gets full control and full responsibility in the same transaction. They choose the HTTP client, the serialization, the retry strategy, the timeout, the logging. They also choose, usually without knowing it, whether the integration will survive the next deprecation.

The trouble arrives six months later, when the same team has three services calling the same endpoint and each one has quietly grown its own error-handling dialect. One retries on 429. One retries on 500. One logs the response body and moves on. Nobody decided this. It accreted.

The SDK as a place to put institutional knowledge

An SDK is a container for decisions the API vendor has already made on the caller's behalf. Which errors are retryable and which are not. How to paginate without losing a page. How to sign a request so that clock skew does not silently break production. Where to put the idempotency key. These are not glamorous questions, but they are the questions that generate support tickets, and an SDK is a way to answer them once and distribute the answer to everyone.

The value is not in the abstraction. It is in the defaults. A well-built client library ships with a retry policy that has been tested against the service's actual failure modes, a timeout that matches the service's real tail latency, and a logging hook that emits structured data instead of a string that someone has to parse later. The caller inherits all of it by installing a package.

That inheritance is the whole point. It is also the source of the risk. Every default is a decision the caller did not make and may not notice until it bites.

The compatibility surface is wider than it looks

An HTTP API has one compatibility surface: the wire contract. Change a field name, change a status code, add a required header, and the contract has moved. Versioning is painful but conceptually clean.

An SDK has at least four surfaces. There is the wire contract underneath. There is the public API of the library itself, the classes, functions, and types that callers import. There is the runtime contract: which language versions are supported, which dependency versions are pinned, which platform quirks are handled. And there is the build contract: whether the package installs cleanly on a machine with a different compiler, a different architecture, or a corporate proxy that rewrites TLS certificates.

A change that looks cosmetic in the library's source can break all four at once. Renaming a method is an API break. Bumping a transitive dependency is a runtime break for someone. Switching from a bundled TLS stack to the system one is a build break on a platform nobody on the team owns.

This is why SDKs age faster than APIs. The API can sit unchanged for years while the ecosystem around it churns. The SDK has to keep up with the churn or it becomes the reason callers abandon the service.

Where an SDK earns its keep

Authentication is the clearest case. A service that requires request signing, token refresh, or multi-step handshakes is asking every caller to reimplement security-sensitive code. An SDK centralizes that code and lets it be audited once. The alternative is a thousand slightly different signing implementations, each with its own subtle bug.

Pagination is the second case. Cursor-based pagination looks simple until a caller has to handle a cursor that expires mid-iteration, a page that returns fewer items than the limit, and a concurrent write that shifts the underlying order. A client library that exposes an iterator handles all of it in one place. Hand-rolled pagination loops are where integrations quietly lose records.

Streaming and long-lived connections are the third. WebSockets, server-sent events, and bidirectional streams each carry reconnection logic, backoff, and state resynchronization. That logic is hard to get right and easy to get almost right. An SDK can ship a reconnection strategy that has been exercised against real network conditions.

Each of these is a place where the caller would otherwise write the same code, badly, in isolation.

Where raw HTTP is the better product

A simple request-response API with a stable JSON schema and bearer-token auth does not need a client library. It needs documentation, a few copy-paste examples, and an OpenAPI spec. Wrapping that in an SDK adds a dependency without removing meaningful work. The caller still has to understand the schema, still has to handle errors, still has to decide on timeouts. The library has absorbed the easy part and left the hard part untouched.

There is also the question of ecosystem breadth. A vendor that ships an SDK for one language is implicitly telling everyone else to use raw HTTP. That is fine if the one language covers most of the audience. It becomes a problem when the SDK is in a language the team does not use, and the presence of an official client creates a false expectation that the other language bindings are equally supported. A half-maintained SDK in a second language is worse than no SDK at all, because it looks maintained until it is not.

And then there is the constraint problem. Some callers operate in environments where adding a dependency is expensive: embedded systems, plugins running inside a host application with a fixed runtime, build pipelines with strict supply-chain review. For them, raw HTTP is not a fallback. It is the only viable path, and the documentation has to be good enough to support it.

The maintenance bill comes due in year two

The first version of an SDK is a joy to build. The API surface is small, the use cases are known, and the team is close to the problem. The second year is where the arithmetic changes.

There are now multiple language versions. Each has its own CI matrix. Each has its own set of open issues. The team that built the first one has partially rotated onto other work, and the remaining maintainer is answering questions about a dependency conflict in a framework version that shipped eight months ago. Meanwhile the underlying API has added two endpoints and deprecated one, and three of the four SDKs have not caught up.

None of this is a reason to avoid shipping an SDK. It is a reason to ship fewer of them and to be honest about which ones are supported. One well-maintained client in the dominant language, with a clear statement that other languages should use raw HTTP, is a defensible position. Four half-maintained clients in four languages is a support burden dressed up as developer relations.

A decision that names its own cost

The useful question is not whether an SDK is better than raw HTTP. It is which specific work the SDK removes from the caller, and whether the vendor is prepared to keep removing it for the life of the service. Authentication, pagination, and streaming are concrete answers with concrete costs. A library that only wraps a POST request is a cost with no corresponding benefit, and it will be judged accordingly the first time a caller has to debug it.

A team that ships an SDK should be able to point at the code it saves the caller from writing, the bug class it prevents, and the release cadence it can sustain. If those three answers are not available, the honest move is to publish the OpenAPI spec, write the curl examples, and let the ecosystem build what it needs. The endpoint was the product all along.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button