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

# Server-side API

> The operations that run only on your backend with the secret Org-API-Key: token exchange, point minting, member management, analytics, and webhooks.

<Warning>
  This page covers **server-side** operations. They use the secret **Org-API-Key**
  on your backend only - **never** ship it in an app, a mobile binary, or any
  client-side code. A request to any of these endpoints with a **member token**
  is rejected with `403`. See [Authentication](/authentication).
</Warning>

Expys splits into two modes. Most data calls an app makes - browsing offers,
redeeming, eligibility, wallet reads, concierge - run in **member mode** with a
short-lived member token. A smaller, privileged set of operations run in
**server mode** with the Org-API-Key, and those must execute only on a backend
you control.

## Which operations are server-side

Every operation below requires the Org-API-Key. A member token on any of them
returns `403`.

| Operation                   | SDK method                                                            | Documented in                                  |
| --------------------------- | --------------------------------------------------------------------- | ---------------------------------------------- |
| Exchange for a member token | `exchangeToken(...)`                                                  | [Authentication](/authentication)              |
| Credit points / mint        | `creditPoints(...)`                                                   | [Points and wallet](/guides/points-and-wallet) |
| Upsert a member             | `setMember(...)`                                                      | [Members and tiers](/guides/members)           |
| Read a member summary       | `getMember(...)`                                                      | [Members and tiers](/guides/members)           |
| Remove a member             | `removeMember(...)`                                                   | [Members and tiers](/guides/members)           |
| Program analytics           | `analyticsSummary()`, `analyticsOffers()`, `analyticsTimeseries(...)` | [Analytics](/guides/analytics)                 |
| Manage webhooks             | `createWebhook(...)`, list, delete                                    | [Webhooks](/guides/webhooks)                   |

## Why the boundary exists

The two credentials carry very different authority, and that difference is the
whole reason for the split.

<CardGroup cols={2}>
  <Card title="Org-API-Key" icon="key">
    A long-lived secret with **full authority over your org**: it can mint tokens
    for any member, move points, change tiers, and read every member's data. It
    must stay on your backend.
  </Card>

  <Card title="Member token" icon="user">
    A **short-lived** credential scoped to a **single member**. If it leaks it
    exposes one member for a few minutes; it cannot touch other members or any
    org-wide operation.
  </Card>
</CardGroup>

Because the Org-API-Key can do anything, it never leaves your backend. The app
only ever holds member tokens, which your backend mints on demand with
`exchangeToken`.

## Member mode vs server mode

|                    | Member mode                                                                  | Server mode                                                                       |
| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Credential         | Member token (Bearer)                                                        | Org-API-Key (Bearer)                                                              |
| Runs in            | The app / SDK client                                                         | Your backend only                                                                 |
| Scope              | One member, short-lived                                                      | The whole org                                                                     |
| Example operations | `listOffers`, `createRedemption`, `checkEligibility`, `getWallet`, concierge | `exchangeToken`, `creditPoints`, `setMember`, `analyticsSummary`, `createWebhook` |
| Wrong credential   | Server-side endpoints return `403`                                           | -                                                                                 |

## Construct the server client

Initialize the SDK with the Org-API-Key as its token. The server-mode methods
are additionally guarded client-side against being called with a member token.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { initialize } from "@expys/sdk";

  const orgApiKey = process.env.EXPYS_ORG_API_KEY;
  if (!orgApiKey) {
    throw new Error(
      "Set EXPYS_ORG_API_KEY (your secret Org-API-Key, e.g. expys_live_...). " +
        "Run this on a backend only, never in a client app.",
    );
  }

  // Construct the client with the machine credential as the token. Server-mode
  // methods are guarded against member tokens client-side.
  const expys = initialize({
    baseUrl: process.env.EXPYS_BASE_URL,
    environment: "sandbox",
    token: orgApiKey,
  });

  const externalUserID = process.env.EXPYS_EXTERNAL_USER_ID ?? "user_42";

  async function main(): Promise<void> {
    // Mint a short-lived member token for your app to use (return this to the app,
    // never the Org-API-Key). Idempotent POST: a retry replays rather than re-mints.
    const grant = await expys.exchangeToken({ externalUserID });
    console.log(`minted member token expiring at ${grant.expiresAt}`);

    // Upsert the member's profile. PUT is idempotent by HTTP semantics (no key).
    const member = await expys.setMember(externalUserID, {
      displayName: "Ada Lovelace",
      tier: "gold",
    });
    console.log(`member ${member.externalUserID} is now tier=${member.tier}`);

    // Credit points to the member's wallet. Idempotent POST sends an Idempotency-Key.
    const credited = await expys.creditPoints({
      amount: 100,
      externalUserID,
      reason: "welcome bonus",
    });
    console.log(`new balance: ${credited.balance} ${credited.currency.symbol}`);

    // Register a webhook. The signingSecret is shown ONLY on creation - store it now.
    const webhook = await expys.createWebhook({
      events: ["redemption.created"],
      url: "https://example.com/expys/webhooks",
    });
    console.log(`webhook ${webhook.id} secret: ${webhook.signingSecret}`);

    // Org-wide analytics rollups.
    const summary = await expys.analyticsSummary();
    console.log(
      `members: ${summary.memberCount}, minted: ${summary.pointsMinted}`,
    );
  }
  ```

  ```swift Swift theme={null}
  import ExpysSDK
  import Foundation

  @main
  struct ServerModeExample {
    static func main() async throws {
      let environment = ProcessInfo.processInfo.environment
      guard let orgApiKey = environment["EXPYS_ORG_API_KEY"] else {
        fatalError(
          "Set EXPYS_ORG_API_KEY (your secret Org-API-Key, e.g. expys_live_...). "
            + "Run this on a backend only, never in a client app.")
      }
      let baseURL =
        environment["EXPYS_BASE_URL"].flatMap(URL.init(string:))
        ?? ExpysConfiguration.defaultBaseURL
      let externalUserID = environment["EXPYS_EXTERNAL_USER_ID"] ?? "user_42"

      // Configure the client with the machine credential as the token. Server-mode
      // methods are guarded against member tokens client-side.
      let client = ExpysClient(
        configuration: ExpysConfiguration(token: orgApiKey, environment: .sandbox, baseURL: baseURL)
      )

      // Mint a short-lived member token for your app to use (return this to the app,
      // never the Org-API-Key). Idempotent POST: a retry replays rather than re-mints.
      let grant = try await client.exchangeToken(TokenExchangeRequest(externalUserID: externalUserID))
      print("minted member token expiring at \(grant.expiresAt)")

      // Upsert the member's profile. PUT is idempotent by HTTP semantics (no key).
      let member = try await client.setMember(
        externalUserID: externalUserID,
        SetMemberRequest(displayName: "Ada Lovelace", tier: "gold"))
      print("member \(member.externalUserID) is now tier=\(member.tier)")

      // Credit points to the member's wallet. Idempotent POST sends an Idempotency-Key.
      let credited = try await client.creditPoints(
        CreditWalletRequest(amount: 100, externalUserID: externalUserID, reason: "welcome bonus"))
      print("new balance: \(credited.balance) \(credited.currency.symbol)")

      // Register a webhook. The signingSecret is shown ONLY on creation - store it now.
      let webhook = try await client.createWebhook(
        CreateWebhookRequest(
          events: ["redemption.created"], url: "https://example.com/expys/webhooks"))
      print("webhook \(webhook.id) secret: \(webhook.signingSecret)")

      // Org-wide analytics rollups.
      let summary = try await client.analyticsSummary()
      print("members: \(summary.memberCount), minted: \(summary.pointsMinted)")
    }
  }
  ```

  ```kotlin Kotlin theme={null}
  package com.expys.sdk.examples.servermode

  import com.expys.sdk.ExpysClient
  import com.expys.sdk.ExpysConfiguration
  import com.expys.sdk.ExpysEnvironment
  import com.expys.sdk.models.CreateWebhookRequest
  import com.expys.sdk.models.CreditWalletRequest
  import com.expys.sdk.models.SetMemberRequest
  import com.expys.sdk.models.TokenExchangeRequest
  import kotlinx.coroutines.runBlocking
  import java.net.URI

  fun main() = runBlocking {
    val orgApiKey = System.getenv("EXPYS_ORG_API_KEY")
      ?: error(
        "Set EXPYS_ORG_API_KEY (your secret Org-API-Key, e.g. expys_live_...). " +
          "Run this on a backend only, never in a client app.",
      )
    val externalUserID = System.getenv("EXPYS_EXTERNAL_USER_ID") ?: "user_42"

    // Configure the client with the machine credential as the token. Server-mode
    // methods are guarded against member tokens client-side.
    val client = ExpysClient.create(
      ExpysConfiguration(
        token = orgApiKey,
        environment = ExpysEnvironment.SANDBOX,
        baseUrl = System.getenv("EXPYS_BASE_URL") ?: ExpysConfiguration.DEFAULT_BASE_URL,
      ),
    )

    // Mint a short-lived member token for your app to use (return this to the app,
    // never the Org-API-Key). Idempotent POST: a retry replays rather than re-mints.
    val grant = client.exchangeToken(TokenExchangeRequest(externalUserID = externalUserID))
    println("minted member token expiring at ${grant.expiresAt}")

    // Upsert the member's profile. PUT is idempotent by HTTP semantics (no key).
    val member = client.setMember(externalUserID, SetMemberRequest(displayName = "Ada Lovelace", tier = "gold"))
    println("member ${member.externalUserID} is now tier=${member.tier}")

    // Credit points to the member's wallet. Idempotent POST sends an Idempotency-Key.
    val credited = client.creditPoints(
      CreditWalletRequest(amount = 100, externalUserID = externalUserID, reason = "welcome bonus"),
    )
    println("new balance: ${credited.balance} ${credited.currency.symbol}")

    // Register a webhook. The signingSecret is shown ONLY on creation - store it now.
    val webhook = client.createWebhook(
      CreateWebhookRequest(events = listOf("redemption.created"), url = URI("https://example.com/expys/webhooks")),
    )
    println("webhook ${webhook.id} secret: ${webhook.signingSecret}")

    // Org-wide analytics rollups.
    val summary = client.analyticsSummary()
    println("members: ${summary.memberCount}, minted: ${summary.pointsMinted}")
  }
  ```
</CodeGroup>

```bash curl 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" }'
```

<Note>
  The `403` is enforced on the server: even if a member token reached one of
  these endpoints, the API rejects it. The client-side guard in the SDK is a
  second layer that fails fast before the request is sent.
</Note>

## Security checklist

<Check>The Org-API-Key is read only on your backend, never bundled into an app.</Check>
<Check>The app receives only member tokens (from `exchangeToken`), with their expiry.</Check>
<Check>Server-side calls - mint, credit, member management, analytics, webhooks - originate from your backend.</Check>
<Check>The Org-API-Key is stored as a backend secret, not in client-shipped environment variables or source.</Check>
<Check>If an Org-API-Key is ever exposed, rotate it immediately - it carries full org authority.</Check>

## Server-side pages

<CardGroup cols={2}>
  <Card title="Authentication" icon="lock" href="/authentication">
    Exchange the Org-API-Key for short-lived member tokens and refresh them.
  </Card>

  <Card title="Members and tiers" icon="users" href="/guides/members">
    Upsert profiles and tiers, read member summaries, and remove members.
  </Card>

  <Card title="Points and wallet" icon="coins" href="/guides/points-and-wallet">
    Mint and credit points into a member's wallet from your backend.
  </Card>

  <Card title="Analytics" icon="chart-line" href="/guides/analytics">
    Program-wide rollups: summary, per-offer, and timeseries.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/guides/webhooks">
    Register signed, retried event deliveries to your backend.
  </Card>
</CardGroup>
