> ## 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.

# SDKs overview

> Three first-class SDKs - TypeScript, Swift, and Kotlin - that share one method surface, one configuration vocabulary, and one error taxonomy.

Expys ships three official data SDKs. They are designed to feel like one library:
the same method names, the same configuration options, and the same error
taxonomy in every language. A concept you learn once - cursor pagination,
automatic idempotency, token refresh, typed errors - applies unchanged across all
three.

Each SDK is **fetch-only** with **zero runtime dependencies** beyond its
platform's HTTP stack, holds a short-lived **member token** (never your
Org-API-Key), and is currently in **beta**: the generated models and transport
are stable to use while the ergonomic layer hardens. Pin an exact version in
production and review the [versioning policy](/guides/versioning).

<CardGroup cols={3}>
  <Card title="TypeScript" icon="js" href="/sdks/typescript">
    `@expys/sdk` on npm. Works in browsers, Expo / React Native, and Node 18+.
  </Card>

  <Card title="Swift" icon="swift" href="/sdks/swift">
    `ExpysSDK` via SwiftPM or CocoaPods. iOS 15+, macOS 12+, async/await.
  </Card>

  <Card title="Kotlin" icon="android" href="/sdks/kotlin">
    `com.expys:sdk` on Maven Central. Coroutine-native; one artifact for JVM
    and Android.
  </Card>
</CardGroup>

## Install

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

  ```swift Swift Package Manager theme={null}
  // Package.swift
  .package(url: "https://github.com/Utopia-Members-Club-Inc/expys-swift.git", from: "0.1.0")
  ```

  ```kotlin Gradle theme={null}
  // build.gradle.kts
  dependencies {
    implementation("com.expys:sdk:0.1.0")
  }
  ```
</CodeGroup>

The first published version of every SDK is `0.1.0`. CocoaPods (`pod 'ExpysSDK',
'~> 0.1'`) and Maven coordinates are covered on each language page.

## One method surface, three languages

The member-mode flow - check eligibility, list offers, redeem the first one - is
identical in shape across the SDKs. Here is the same flow in all three:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { ConflictError, 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)",
    );
  }

  const expys = initialize({
    baseUrl: process.env.EXPYS_BASE_URL,
    environment: "sandbox",
    token,
  });

  async function main(): Promise<void> {
    const eligibility = await expys.eligibility();
    console.log(
      `tier: ${eligibility.tier}, balance: ${eligibility.wallet.balance}`,
    );

    const { data: offers } = await expys.listOffers({ limit: 10 });
    console.log(`browsed ${offers.length} offers`);

    const offer = offers[0];
    if (!offer) {
      return;
    }

    console.log(`redeeming: ${offer.title} (${offer.id})`);
    try {
      const redemption = await expys.createRedemption({ offer: offer.id });
      console.log(`redemption created: ${redemption.id} [${redemption.status}]`);

      const status = await expys.getRedemption(redemption.id);
      console.log(`status now: ${status.status}`);
    } catch (error) {
      if (error instanceof ConflictError) {
        console.log(`already redeemed: ${error.code}`);
        return;
      }
      throw error;
    }
  }
  ```

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

  @main
  struct BrowseRedeem {
    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)
      )

      let eligibility = try await client.eligibility()
      print("tier: \(eligibility.tier), balance: \(eligibility.wallet.balance)")

      let offers = try await client.listOffers(limit: 10)
      print("browsed \(offers.data.count) offers")

      guard let offer = offers.data.first else { return }
      print("redeeming: \(offer.title) (\(offer.id))")

      do {
        let redemption = try await client.createRedemption(.init(offer: offer.id))
        print("redemption created: \(redemption.id) [\(redemption.status)]")
        let status = try await client.getRedemption(id: redemption.id)
        print("status now: \(status.status)")
      } catch ExpysError.api(let error) where error.code == "REDEMPTION_ALREADY_EXISTS" {
        print("already redeemed")
      }
    }
  }
  ```

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

  import com.expys.sdk.ExpysClient
  import com.expys.sdk.ExpysConfiguration
  import com.expys.sdk.ExpysEnvironment
  import com.expys.sdk.ExpysException
  import com.expys.sdk.models.CreateRedemptionRequest
  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,
      ),
    )

    val eligibility = client.eligibility()
    println("tier: ${eligibility.tier}, balance: ${eligibility.wallet.balance}")

    val offers = client.listOffers(limit = 10)
    println("browsed ${offers.`data`.size} offers")

    val offer = offers.`data`.firstOrNull() ?: return@runBlocking
    println("redeeming: ${offer.title} (${offer.id})")

    try {
      val redemption = client.createRedemption(CreateRedemptionRequest(offer = offer.id))
      println("redemption created: ${redemption.id} [${redemption.status}]")
      println("status now: ${client.getRedemption(redemption.id).status}")
    } catch (error: ExpysException.Api) {
      if (error.error.code == "REDEMPTION_ALREADY_EXISTS") {
        println("already redeemed")
      } else {
        throw error
      }
    }
  }
  ```
</CodeGroup>

The only intentional concurrency difference is the streaming return type
(`AsyncIterable` / `AsyncThrowingStream` / `Flow`); everything else is guaranteed
identical. See [SDK differences](/sdks/differences) for the full contract.

## What's shared

* **Member-mode and server-mode methods** with the same names everywhere.
  Server-mode methods require an Org-API-Key and must run only on your backend.
* **A common configuration vocabulary**: `token`, `environment`, `baseUrl`,
  `refreshToken`, `tokenExpiresAt`, `maxRetries`, `timeout`, and more. See the
  [configuration reference](/guides/configuration).
* **One error taxonomy**: every API error carries a stable `code`, an HTTP
  `status`, a coarse category, and a `requestId`. See [Errors](/guides/errors).
* **Built-in reliability**: full-jitter retries on `429`/`5xx`, automatic
  `Idempotency-Key` on writes, and proactive plus reactive token refresh.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Install an SDK, mint a member token, and run your first flow.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    The two-token model and the token-refresh contract every SDK shares.
  </Card>

  <Card title="Configuration" icon="sliders" href="/guides/configuration">
    Every option, with its default and per-language type.
  </Card>

  <Card title="SDK differences" icon="code-compare" href="/sdks/differences">
    The handful of intentional, idiomatic per-language differences.
  </Card>
</CardGroup>
