> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The SDK surface model: tiers, shared contract, and codegen

> How Orbit's twelve SDKs are organized — the three tiers (GA server, core-scope server, client-scope), the shared auth/retry/idempotency contract, the OpenAPI codegen mapping, the publishable-vs-secret key split, and the cross-language error hierarchy.

# The SDK surface model

Orbit ships twelve language SDKs, and they are not twelve parallel attempts
at the same thing. They share one architectural model: a fixed split into
three tiers, one request contract every client honors, a shared error
hierarchy, and a deliberate split between what a secret key can do and what
a publishable key can do. This page explains that model. For installation
and a first call in a specific language, go straight to the [SDK index](/sdks/index)
and the per-language pages linked at the end.

## The three tiers

Every SDK occupies exactly one tier, chosen by where it will run and what
credential it can hold:

| Tier | Languages | Scope | Why it differs |
| - | - | - | - |
| GA — feature-complete | [Node.js](/sdks/node), [Web](/sdks/web) | Full resource surface | Node is hand-written and reviewed per resource; Web is the browser widget + inbound surface |
| Core-scope server | [Python](/sdks/python), [Go](/sdks/go), [Ruby](/sdks/ruby), [PHP](/sdks/php), [Java](/sdks/java), [.NET](/sdks/csharp) | The 8 core resources: messaging, voice, contacts, campaigns, verify, numbers, webhook verification | Server-side clients built from the codegen pipeline; the long tail of sub-resources (RCS, agents, billing, voice IVR/conferences/dialer) is intentionally out of scope and reachable via the escape hatch |
| Client-scope | [Web widget](/sdks/web), [Swift](/sdks/swift), [Android](/sdks/android), [Flutter](/sdks/flutter), [React Native](/sdks/react-native) | End-user surfaces only: conversations/inbox chat, in-app messages, video rooms, voice agent, (Swift) VoIP push registration | These ship inside a distributed app binary, so they authenticate with a publishable key and deliberately omit every backend resource |

The split is a design decision, not a backlog. A client-scope SDK that
exposed `campaigns` or `billing` would be embedding a server credential in
an installable binary; a core-scope SDK wrapping every niche sub-resource
would multiply the maintenance surface without adding capability, because
the escape hatch (below) already covers those routes.

## The shared contract every SDK saturates

Whether the client is hand-written (Node) or codegen-wrapped (Python, Go,
Ruby, PHP, Java, .NET), these behaviors are identical:

* **Auth header**: every request carries `X-API-Key: <key>`. Server
  SDKs expect a secret key; client-scope SDKs use the publishable key and
  call only public, key-tolerant surfaces.
* **Retry policy**: non-2xx responses classified as retryable — a `429`
  or any `5xx` — are retried up to 3 attempts per request by default,
  with exponential backoff starting at 250 ms and doubling each round,
  honoring the server's `Retry-After` header and clamped at a 60-second
  ceiling. `4xx` responses other than `429` are surfaced immediately.
* **Idempotency**: every mutating request (`POST`, `PUT`, `PATCH`,
  `DELETE`) carries an `Idempotency-Key` header. The SDK generates a
  random UUID once per logical call and reuses it across the retry chain,
  so a retried send collapses to one charge on the server. You can pin
  your own key — in Node, pass `idempotency` on the send input.
* **Webhook verification**: server-side SDKs expose a one-call verifier
  over the `Orbit-Signature` header (`t=<unix_ts>,v1=<hex_hmac>`,
  HMAC-SHA256 with your webhook secret, 5-minute replay window).

<Note>
  Webhook verification is server-side only. Client-scope SDKs never
  receive inbound webhooks, so none of them ships a verifier.
</Note>

## How the codegen pipeline maps OpenAPI onto typed clients

The single source of truth is `apps/api/openapi.yaml`, and the pipeline at
`tools/sdk-codegen` turns it into the six non-Node server SDKs (Python,
Go, Ruby, PHP, Java, .NET). The shape we ship is two layers:

1. **Generated layer**: `packages/sdk-<lang>/generated/` holds raw
   openapi-generator output — verbose, auto-named, wiped and re-emitted
   from the spec on every `pnpm sdk:codegen` run. Customers never import
   from it.
2. **Handcrafted wrapper**: `packages/sdk-<lang>/src/` (or `lib/`) holds
   the `OrbitClient` constructor, the auth helper, the error hierarchy,
   and the typed resource methods customers actually call. The wrapper
   imports types and low-level operations from the generated layer.

The wrapper is where an OpenAPI `operationId` becomes an idiomatic method.
Regeneration is idempotent, and a spec-drift gate
(`pnpm sdk:check-spec-drift <lang>`) fails a release when the published
spec's hash no longer matches the language's pinned hash — so a route
rename can't slip through as a customer 404.

### When the escape hatch is the right tool

Each core-scope client also exposes a low-level, untyped request helper —
`client.request` in Node/Python/Java/Ruby, `client.Request*` in Go/.NET —
that applies the full contract (auth header, retries, idempotency, error
parsing) to any path. Use it for exactly two cases:

* a **covered route the typed resources don't wrap yet** (RCS, agents,
  billing, voice IVR/dialer): the helper keeps the contract without
  pretending the route has a typed shape;
* a **versioned preview or admin route** you don't want to hand-build
  headers for.

Anything stable belongs in a typed method; the escape hatch exists so an
unwrapped route never forces you to drop down to raw `fetch`.

### Worked example: `POST /messages/sms`

The same endpoint at each layer, showing what the codegen-to-wrapper
mapping produces end to end.

**1. The OpenAPI operation** (`apps/api/openapi.yaml`):

```yaml theme={null}
/api/v1/messages/sms:
  post:
    operationId: sendSmsMessage
    summary: Send an SMS / MMS message
    requestBody:
      content:
        application/json:
          schema:
            type: object
            properties:
              to:    { type: string }
              body:  { type: string }
              from:  { type: string }
              media_url: { type: string }
```

**2. The typed Node method** — the codegen pipeline's `operationId` ends
as an idiomatic wrapper method:

```typescript theme={null}
const sent = await orbit.messages.sendSms({
  to: "+14155552671",
  body: "Hello from Orbit!",
});
// POST /api/v1/messages/sms → { data: { id: "msg_" ... status: "queued" } }
```

**3. The request the client actually sends** — the raw fetch shape after
the wrapper applies the shared contract:

```text theme={null}
POST /api/v1/messages/sms
Headers:
  X-API-Key: dv_live_sk_...
  Content-Type: application/json
  User-Agent: orbit-node-sdk/<version>
  Idempotency-Key: 9f8b...-random-uuid (generated once, reused across retries)
Body:
  {"to":"+14155552671","body":"Hello from Orbit!"}
```

Any failure along that chain surfaces as a subclass of `OrbitApiError`
(below) carrying `statusCode`, `code`, `requestId`, and on `429` the
`Retry-After` hint in `details`.

## Publishable key vs. secret key

Orbit issues two key envelopes, and which one a client-scope SDK is
allowed to hold is the whole point of the envelope split:

* **Secret key (`dv_live_sk_…`)** — full API access. Server-side only:
  it must never ship inside a mobile app, a browser bundle, a public
  repository, or a client-side environment variable. Every server SDK
  (GA Node, core-scope languages) authenticates with this.
* **Publishable key (`dv_live_pk_…`)** — public ingest + client-scope
  surfaces only: identified/anonymous event ingest (`/sdk/identify`,
  `/sdk/track`), conversations/inbox chat, in-app content cards, video
  rooms, and the voice-agent widget. Safe to embed in a distributed
  binary.

A client-scope SDK is deliberately unable to call any backend or
management resource — `messaging`, `whatsapp`, `email`, `contacts`,
`campaigns`, `verify`, `billing`, and every admin route — because those
require the secret key. If you need them from a backend, use the [Node
SDK](/sdks/node) or call the REST API directly. A leaked publishable key
exposes only anonymous ingest, which is rate-limited per-key; a leaked
secret key exposes your whole tenant, so the envelope split exists.

## The error hierarchy

Every SDK exposes the same tree, so a `catch` pattern you write in Node
ports to Python unchanged:

* `OrbitApiError` (base) — catches any non-2xx API response. Carries
  `code`, `statusCode` (aliased as `status`), `details`, `docsUrl`, and
  `requestId`, plus `isRateLimited` / `isClientError` / `isServerError`
  getters.
* `OrbitAuthError` — 401/403: bad key or missing scope.
* `OrbitNotFoundError` — 404: resource absent or not visible.
* `OrbitRateLimitError` — 429: retryable; check `Retry-After` in `details`
  before retrying.
* `OrbitValidationError` — 400/422: the request body failed validation;
  fix it, don't retry.
* `OrbitServerError` — 5xx after retries exhausted, or persistent network
  failure.
* `OrbitWebhookSignatureError` — webhook verification failed, with a
  `code` of `bad_signature | expired | malformed`. It extends `Error`,
  **not** `OrbitApiError`, so a broad `catch (OrbitApiError)` block can't
  accidentally retry a forged webhook.

## Where this page ends and the quickstarts begin

Use the model above to choose a tier, then pick a language:

* **GA server**: [Node.js](/sdks/node), [Web](/sdks/web)
* **Core-scope server**: [Python](/sdks/python), [Go](/sdks/go),
  [Ruby](/sdks/ruby), [PHP](/sdks/php), [Java](/sdks/java),
  [.NET](/sdks/csharp)
* **Client-scope**: [Web](/sdks/web), [Swift](/sdks/swift),
  [Android](/sdks/android), [Flutter](/sdks/flutter),
  [React Native](/sdks/react-native)

Each per-language page carries the install command, the first-send
quickstart, an OTP verify round trip, and a paginated list example. The
[SDK index](/sdks/index) has registry status per language, and
[Per-language recipes](/guides/per-language-recipes) repeats the core
recipes as tabs across cURL and all eight server SDKs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.