Skip to main content
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.
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.

Install

The artifact is published to Maven Central as com.expys:sdk.
// build.gradle.kts
dependencies {
  implementation("com.expys:sdk:0.1.0")
}

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.
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 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:
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`);
}
The full option table, with defaults and per-language types, lives in the configuration reference.

Errors

Calls throw ExpysException. Catch ExpysException.Api and branch on the error’s kind and stable code:
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.

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.

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

Authentication

Minting member tokens and the refresh lambda.

Configuration

Every option with its default and Kotlin type.

Streaming

Collecting the concierge message Flow.

API reference

Every endpoint, with a live “Try it” playground.