# How We Generate Our SDKs from One OpenAPI Spec

> Why the otp.com client libraries for Node, Python, Go, and PHP are generated from one OpenAPI spec instead of written by hand, and how that stops drift.

- Category: Engineering
- Author: otp.com Team
- Published: 2026-07-16
- Tags: sdks, developer-experience
- Canonical: https://otp.com/blog/engineering/generating-sdks-from-openapi

---

Hand-written client libraries are a maintenance trap. Ship a new field on the API and you now have
four libraries to update by hand, four changelogs to write, and four chances to drift apart from
what the docs promise and what the client actually sends.

So we don't write our SDKs by hand. There is one source of truth, the public **OpenAPI spec**, and
the Node.js, Python, Go, and PHP clients are *generated* from it.

## One spec, four clients

The API exposes four actions, and so does every SDK:

- `send`: send a code (your account routing picks the channel)
- `verify`: check the code the user entered
- `resend`: retry on the next channel
- `status`: look up an OTP's state

Generate from the spec and each language gets those four as typed methods (`sendOtp`, `verifyOtp`,
`resendOtp`, `getOtpStatus` in Node; `send_otp`, `verify_otp`, … in Python). Auth is the same
everywhere: your API key as a Bearer token, base URL `https://api.otp.com/api/v1`.

```ts
import { Configuration, OTPApi } from '@otp.com/sdk-node';

const api = new OTPApi(new Configuration({ accessToken: process.env.OTP_API_KEY }));

const sent = await api.sendOtp({ sendOtpRequest: { recipient: '+14155552671', locale: 'en' } });
const result = await api.verifyOtp({ verifyOtpRequest: { otpId: sent.otpId, code: '123456' } });
// result.matched === true
```

## How the pipeline works

The flow has one direction: the spec describes the API, a generator turns it into client code for
each target language, and those clients get published to their ecosystems (npm, PyPI, Go modules,
Composer). Nobody edits the generated output by hand between those steps, because a hand-edit is a
drift waiting to happen: the next regeneration would either overwrite it or, worse, be held back to
preserve it. The spec is the code's parent, not its sibling.

That constraint is the whole point. It means the request and response shapes in every SDK are the
same shapes the API actually serves, because they came from the same document the server is built
against.

## Why generated beats hand-written

- **No drift.** The client's request and response types *are* the spec. If the wire format changes,
  the types change with it, so a mismatch fails to compile instead of failing in production.
- **Full coverage, instantly.** A new parameter or endpoint appears in every SDK the moment the spec
  does, not whenever someone gets around to porting it.
- **One review surface.** Behavior is reviewed once, on the spec, instead of re-litigated in four
  languages.
- **Honest types.** `verify` never returns the code (you match on `otpId`), and the generated types
  reflect that; you can't accidentally depend on a field that doesn't exist.

## When the API changes

Adding a field is the routine case, and it shows the model at its best. The spec gets the field, the
clients regenerate, and the new option is available in Node, Python, Go, and PHP in one pass, with
its type, not four separate ports landing weeks apart. Because the API is versioned under `/api/v1`,
changes are additive within a version, so regenerating picks up new capability without breaking the
method calls you already wrote.

## The trade-off

Generated code isn't always the most idiomatic code a human would hand-write for a given language,
and we take that on purpose: predictable and in-sync beats hand-crafted and drifting. Where
ergonomics matter, the method shape stays close to the REST call you'd otherwise make yourself, so
the mental model transfers whether you read the SDK or the raw endpoint.

## The spec is public

Because the OpenAPI document is public, you are not limited to the four languages we publish. If you
work in a language we don't ship, you can point a generator at the same spec and get a typed client
of your own, or simply call the [REST API](/docs/api/send) directly, which works everywhere. The
generated SDKs are a convenience built on an open contract, not a gate in front of it.

## Getting a client

- Node.js: `npm install @otp.com/sdk-node`
- Python: `pip install otp-sdk`
- Go: `go get github.com/otp-com/sdk-go`
- PHP: `composer require otp-com/sdk-php`

If your language isn't on the list, the [REST API](/docs/api/send) works everywhere. Building with
an AI agent instead of writing integration code? The [MCP server](/docs/mcp) exposes the same four
actions as agent tools.
