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

# Swift SDK

> Install ExpysSDK via SwiftPM or CocoaPods and embed Expys into your iOS, macOS, or server-side Swift app - async/await, URLSession, zero runtime dependencies.

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.

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

<Note>
  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.
</Note>

## Install

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

  ```ruby CocoaPods theme={null}
  pod 'ExpysSDK', '~> 0.1'
  ```
</CodeGroup>

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.

```swift theme={null}
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](/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:

<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 `ExpysError`. Switch on `ExpysError.api(let error)` and refine with
the error's `kind` and stable `code`:

```swift theme={null}
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](/guides/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](/guides/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

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

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

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

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