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

# TypeScript SDK

> Install @expys/sdk and embed Expys into your web, React Native, or Node app - fetch-only, with zero runtime dependencies and types included.

The official Expys data SDK for TypeScript. Embed Expys experiences into your web,
React Native, or Node app. It is fetch-only with **zero runtime dependencies**,
and ships its own types.

<Info>
  Beta. The generated types and transport are stable to use; the ergonomic layer
  is hardening during the rollout window. Pin an exact version in production and
  review the [versioning policy](/guides/versioning).
</Info>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @expys/sdk
  ```

  ```bash bun theme={null}
  bun add @expys/sdk
  ```

  ```bash yarn theme={null}
  yarn add @expys/sdk
  ```

  ```bash pnpm theme={null}
  pnpm add @expys/sdk
  ```
</CodeGroup>

## Initialize

The SDK is constructed with `initialize({ token, environment, ... })`. The
`token` is a short-lived **member token** your backend obtained from
`POST /v1/auth/exchange` - never your Org-API-Key.

```ts theme={null}
import { initialize } from "@expys/sdk";

const expys = initialize({
  token: memberToken,
  environment: "live", // or "sandbox"
});

const { data: offers } = await expys.listOffers({ limit: 20 });
```

Provide a `refreshToken` hook and the SDK refreshes the member token
automatically near expiry and once on a `401`. The hook must call **your**
backend (which re-exchanges the Org-API-Key) and return
`{ accessToken, expiresAt? }`. See [Authentication](/authentication) for the full
contract.

## Configuration

`initialize` accepts the shared configuration vocabulary - `baseUrl`,
`maxRetries`, `timeoutMs`, a custom `fetch`, and more. Here it is alongside the
Swift and Kotlin equivalents:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { initialize } from "@expys/sdk";

  const token = process.env.EXPYS_MEMBER_TOKEN;
  if (!token) {
    throw new Error(
      "Set EXPYS_MEMBER_TOKEN (a member token from your backend's /v1/auth/exchange)",
    );
  }

  // A custom fetch wrapper: log each request, then delegate to the platform fetch.
  // Use this seam for tracing, metrics, or a polyfill on Node < 18. The cast keeps
  // it simple here under bun-types (whose `fetch` carries extra members); in a
  // typical web/Node project the arrow satisfies `typeof fetch` without a cast.
  const instrumentedFetch = ((input, init) => {
    const method = init?.method ?? "GET";
    const url =
      typeof input === "string"
        ? input
        : input instanceof URL
          ? input.href
          : input.url;
    console.log(`-> ${method} ${url}`);
    return fetch(input, init);
  }) as typeof fetch;

  const expys = initialize({
    baseUrl: process.env.EXPYS_BASE_URL,
    environment: "sandbox",
    fetch: instrumentedFetch,
    // Retry 429/5xx up to 3 extra times (4 attempts total) with backoff.
    maxRetries: 3,
    // Abort any single attempt that exceeds 8s.
    timeoutMs: 8_000,
    token,
  });

  async function main(): Promise<void> {
    const { data } = await expys.listOffers({ limit: 3 });
    console.log(`fetched ${data.length} offers with the configured client`);
  }
  ```

  ```swift Swift theme={null}
  import ExpysSDK
  import Foundation

  #if canImport(FoundationNetworking)
    import FoundationNetworking
  #endif

  /// A custom HTTP layer that logs each request, then delegates to URLSession. Use
  /// this seam (the `httpClient` injection point) for tracing, metrics, or a custom
  /// transport - the Swift analogue of the TS example's instrumented `fetch`.
  struct InstrumentedHTTP: HTTPRequesting {
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
      print("-> \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")")
      return try await URLSession.shared.data(for: request)
    }
  }

  @main
  struct ConfigurationExample {
    static func main() async throws {
      let environment = ProcessInfo.processInfo.environment
      guard let token = environment["EXPYS_MEMBER_TOKEN"] else {
        fatalError("Set EXPYS_MEMBER_TOKEN (a member token from your backend's /v1/auth/exchange)")
      }
      let baseURL =
        environment["EXPYS_BASE_URL"].flatMap(URL.init(string:))
        ?? ExpysConfiguration.defaultBaseURL

      let client = ExpysClient(
        configuration: ExpysConfiguration(
          token: token,
          environment: .sandbox,
          baseURL: baseURL,
          // Retry 429/5xx up to 3 extra times (4 attempts total) with backoff.
          maxRetries: 3,
          // Abort any single attempt that exceeds 8s.
          timeout: 8
        ),
        httpClient: InstrumentedHTTP()
      )

      let offers = try await client.listOffers(limit: 3)
      print("fetched \(offers.data.count) offers with the configured client")
    }
  }
  ```

  ```kotlin Kotlin theme={null}
  package com.expys.sdk.examples.configuration

  import com.expys.sdk.ExpysClient
  import com.expys.sdk.ExpysConfiguration
  import com.expys.sdk.ExpysEnvironment
  import kotlinx.coroutines.runBlocking

  fun main() = runBlocking {
    val token = System.getenv("EXPYS_MEMBER_TOKEN")
      ?: error("Set EXPYS_MEMBER_TOKEN (a member token from your backend's /v1/auth/exchange)")

    val client = ExpysClient.create(
      ExpysConfiguration(
        token = token,
        environment = ExpysEnvironment.SANDBOX,
        baseUrl = System.getenv("EXPYS_BASE_URL") ?: ExpysConfiguration.DEFAULT_BASE_URL,
        orgId = System.getenv("EXPYS_ORG_ID"),
        maxRetries = 3,
        timeoutMs = 10_000,
        refreshSkewMs = 30_000,
        userAgentSuffix = "my-app/1.0",
      ),
    )

    println("offers: ${client.listOffers(limit = 1).`data`.size}")
  }
  ```
</CodeGroup>

The full option table, with defaults and per-language types, lives in the
[configuration reference](/guides/configuration).

## Errors

Every failed request throws a typed error carrying the stable envelope `code`.
Branch on the class, then refine with the code:

```ts theme={null}
import { ConflictError, RateLimitError } from "@expys/sdk";

try {
  await expys.createRedemption({ offer });
} catch (error) {
  if (error instanceof ConflictError && error.code === "REDEMPTION_ALREADY_EXISTS") {
    // the member already booked this offer
  }
  if (error instanceof RateLimitError) {
    // error.retryAfterMs is set
  }
}
```

The classes are `ApiError` (base for HTTP errors) and `UnauthorizedError`,
`ForbiddenError`, `NotFoundError`, `ConflictError`, `ValidationError`,
`RateLimitError`, `ServerError`, plus `NetworkError` / `TimeoutError` and the
common base `ExpysError`. Treat an unknown `code` as the generic class for its
status - new codes can appear in a minor release. See [Errors](/guides/errors).

## Runtime support

Works anywhere a standard `fetch` exists: modern browsers (Vite / web), Expo /
React Native, and Node 18+. Provide a `fetch` polyfill for older runtimes; the
built package is smoke-tested on Node 18 / 20 / 22, Bun, and Deno. The SDK sends
no telemetry.

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    Minting member tokens and the refresh hook.
  </Card>

  <Card title="Configuration" icon="sliders" href="/guides/configuration">
    Every option with its default.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/guides/errors">
    The error taxonomy and stable codes.
  </Card>

  <Card title="API reference" icon="terminal" href="/api-reference/introduction">
    Every endpoint, with a live "Try it" playground.
  </Card>
</CardGroup>
