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

# Kotlin SDK

> Install com.expys:sdk from Maven Central and embed Expys into your Android or JVM app - coroutine-native, OkHttp, one artifact for server and Android.

The official Expys data SDK for Kotlin/JVM: coroutine-native (suspend), OkHttp,
and kotlinx.serialization. A single pure-Kotlin/JVM artifact (no Android
resources, no Android Gradle Plugin) that runs identically for Android and server
consumers.

<Info>
  Beta. The generated models 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

The artifact is published to Maven Central as `com.expys:sdk`.

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

  ```xml Maven theme={null}
  <dependency>
    <groupId>com.expys</groupId>
    <artifactId>sdk</artifactId>
    <version>0.1.0</version>
  </dependency>
  ```
</CodeGroup>

## Initialize

The client is constructed with `ExpysClient.create(ExpysConfiguration(...))`. The
`token` is a short-lived **member token** your backend obtained from
`POST /v1/auth/exchange` - never your Org-API-Key.

```kotlin theme={null}
import com.expys.sdk.ExpysClient
import com.expys.sdk.ExpysConfiguration

val client = ExpysClient.create(
  ExpysConfiguration(
    token = memberToken,
    environment = ExpysEnvironment.LIVE, // or SANDBOX
  ),
)

val offers = client.listOffers(limit = 20)
```

All calls are `suspend` functions; call them from a coroutine. Cancellation
propagates cleanly and is never retried. Provide a `refreshToken` lambda and the
SDK refreshes the member token automatically near expiry and once on a `401`; it
must call **your** backend and return a `TokenRefresh(accessToken = ...)`. See
[Authentication](/authentication) for the full contract.

## Configuration

`ExpysConfiguration` carries the shared configuration vocabulary - `baseUrl`,
`maxRetries`, `timeoutMs`, `tokenExpiresAtMs`, `refreshSkewMs`, and more.
`ExpysClient.create(configuration, httpClient)` accepts an optional `HttpClient`
to inject a custom transport for instrumentation or testing. Here it is alongside
the TypeScript and Swift 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

Calls throw `ExpysException`. Catch `ExpysException.Api` and branch on the error's
`kind` and stable `code`:

```kotlin theme={null}
try {
  client.createRedemption(CreateRedemptionRequest(offer = offerId))
} catch (e: ExpysException.Api) {
  when (e.error.kind) {
    ApiErrorKind.CONFLICT -> if (e.error.code == "REDEMPTION_ALREADY_EXISTS") { /* already booked */ }
    ApiErrorKind.RATE_LIMITED -> { /* e.error.retryAfterMs is set */ }
    else -> {}
  }
} catch (e: ExpysException.Timeout) {
  // request timed out
}
```

`ExpysException` subtypes are `Api(ApiError)`, `Network(detail)`, `Timeout`,
`Decoding(detail)`, and `NotConfigured(detail)`. `ApiError` carries `status`, the
stable envelope `code`, `message`, optional `retryAfterMs` (milliseconds, matching
the TS and Swift SDKs), optional `requestId`, and a coarse `kind`. Treat an
unknown code as the generic class for its `kind`. See [Errors](/guides/errors).

## Streaming with Flow

`streamMessages(id)` returns a cold `Flow<Message>` of new, member-visible
concierge messages over Server-Sent Events. Collect it to subscribe; cancelling
collection (for example, cancelling the collecting coroutine's scope) tears down
the connection. See [Streaming](/guides/streaming).

## Platform support

A single pure-Kotlin/JVM artifact that runs unchanged on the server and on
Android.

* **JVM:** Java 17+ (the artifact targets JVM 17 bytecode).
* **Android:** consumable from an app compiling against Java 17 (Android Gradle
  Plugin 8+). The transport is OkHttp 4.12 (Android API 21+).
  * **`minSdk`:** parsing RFC 7231 `Retry-After` dates uses `java.time`, which
    needs **API 26+**, or **API 21+ with core library desugaring** enabled.
  * **R8/ProGuard:** keep rules ship inside the artifact, so R8 applies them
    automatically with no app-side config.

The SDK sends no telemetry.

## Next steps

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

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

  <Card title="Streaming" icon="bolt" href="/guides/streaming">
    Collecting the concierge message Flow.
  </Card>

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