Skip to main content
The official Expys data SDK for Swift: SwiftPM, async/await, URLSession, and zero runtime dependencies (Foundation only). Built in the Swift 6 language mode with complete data-race checking.
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.
The source lives in the Expys monorepo, but releases are mirrored to the public expys-swift repo so SwiftPM can resolve them by git URL plus semver tag. Add the package from the mirror URL below.

Install

// Package.swift
.package(url: "https://github.com/Utopia-Members-Club-Inc/expys-swift.git", from: "0.1.0")
In Xcode, use File -> Add Package Dependencies and paste the expys-swift URL.

Initialize

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

let client = ExpysClient(
  configuration: ExpysConfiguration(
    token: memberToken,
    environment: .live // or .sandbox
  )
)

let offers = try await client.listOffers(limit: 20)
Provide a refreshToken closure 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:expiresAt:). See Authentication for the full contract.

Configuration

ExpysConfiguration carries the shared configuration vocabulary - baseURL, maxRetries, timeout (a TimeInterval in seconds), refreshSkew, and more. An optional httpClient initializer parameter injects a custom HTTPRequesting transport for instrumentation or testing. Here it is alongside the TypeScript and Kotlin 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 ExpysError. Switch on ExpysError.api(let error) and refine with the error’s kind and stable code:
do {
  _ = try await client.createRedemption(.init(offer: offerID))
} catch ExpysError.api(let error) {
  switch error.kind {
  case .conflict where error.code == "REDEMPTION_ALREADY_EXISTS":
    break // already booked
  case .rateLimited:
    break // error.retryAfterMs is set
  default:
    break
  }
} catch ExpysError.timeout {
  // request timed out
}
ExpysError cases are .api(APIError), .network(String), .timeout, .decoding(String), and .notConfigured(String). APIError carries status, the stable envelope code, message, optional retryAfterMs (milliseconds, matching the TS and Kotlin SDKs), optional requestId, and a coarse kind. Treat an unknown code as the generic class for its kind. See Errors.

Streaming

streamMessages(id:) returns an AsyncThrowingStream<Message, Error> of new, member-visible concierge messages over Server-Sent Events. Consume it with for try await; cancelling the consuming Task tears down the connection. See Streaming.

Platform support

iOS 15+ and macOS 12+ (async/await). Linux (Swift 6.1) is supported for server-side use and CI. Requires the Swift 6 toolchain. The SDK sends no telemetry.

Next steps

Authentication

Minting member tokens and the refresh closure.

Configuration

Every option with its default and Swift type.

Streaming

Consuming the concierge message stream.

API reference

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