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

# Quickstart

> Install an SDK, get a member token, and run your first browse-and-redeem flow against the sandbox.

This walks through your first end-to-end VIP flow - browse a curated catalog of
experiences and redeem one - against the **sandbox** environment, which serves a
seeded demo catalog so you can build before wiring up your own data.

<Steps>
  <Step title="Get an Org-API-Key">
    Create a developer account and a **sandbox** key in the
    [developer portal](https://app.expys.com/signup). The key is a server-side
    secret - keep it on your backend.
  </Step>

  <Step title="Install an SDK">
    <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", from: "0.1.0")
      ```

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

  <Step title="Mint a member token from your backend">
    The app never holds the Org-API-Key. Your backend exchanges it for a
    short-lived **member token** scoped to one of your users:

    ```bash theme={null}
    curl -X POST https://api.expys.com/v1/auth/exchange \
      -H "Authorization: Bearer YOUR_ORG_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "externalUserID": "user_123" }'
    ```

    The response is a `TokenGrant` - an `accessToken` and an `expiresAt`. Hand the
    `accessToken` to the app. See [Authentication](/authentication) for the full
    refresh contract.
  </Step>

  <Step title="Browse and redeem">
    Initialize the SDK with the member token and run the data flow - check
    eligibility, list offers, and redeem the first one:

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

    Run it with the member token in the environment:

    ```bash theme={null}
    EXPYS_MEMBER_TOKEN=... <run your example>
    ```
  </Step>
</Steps>

<Check>
  A successful run prints the member's tier and balance, the number of offers
  browsed, and a new redemption id with its status. If a redemption already
  exists for that offer, the SDK raises a `REDEMPTION_ALREADY_EXISTS` conflict -
  see [Errors](/guides/errors).
</Check>

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication and refresh" icon="key" href="/authentication">
    The two-token model and how the SDK refreshes member tokens.
  </Card>

  <Card title="Environments" icon="layer-group" href="/environments">
    How sandbox and live differ, and how the key selects one.
  </Card>

  <Card title="Redemptions" icon="ticket" href="/guides/redemptions">
    Points spend, idempotency, and the redemption lifecycle.
  </Card>

  <Card title="Errors and retries" icon="triangle-exclamation" href="/guides/errors">
    The error taxonomy, stable codes, and retry behavior.
  </Card>
</CardGroup>
