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

# Billing and settlement

> How your prepaid points pool funds VIP experiences, the two settlement modes, and how to top the pool up.

Your organization holds a **prepaid points pool**. Everything your VIPs redeem is
ultimately paid for out of that pool. Sandbox activity is always free and never
touches it.

What differs between organizations is *which layer a redemption debits* - your
**settlement mode**.

## The two settlement modes

<CardGroup cols={2}>
  <Card title="Member wallet" icon="wallet">
    **The default.** Each VIP has their own wallet. You fund a VIP by calling
    `POST /v1/wallet/credit`, which draws your pool down. When that VIP redeems,
    the points come out of *their* wallet.

    Use this when you want per-VIP balances that your users can see and you are
    happy for Expys to hold them.
  </Card>

  <Card title="Org pool" icon="building-columns">
    **No per-VIP balances at all.** You never call `/v1/wallet/credit`. When a VIP
    redeems, the points are debited straight from your org pool, and the booking is
    attributed to that VIP for reporting.

    Use this when you already run your own loyalty balance and do not want to
    mirror or reconcile it with ours.
  </Card>
</CardGroup>

<Note>
  Your settlement mode is configured by Expys, not self-serve. Ask your Expys
  contact to change it. Read your current mode from `GET /v1/balance`.
</Note>

### What org-pool mode changes for you

* **You never credit wallets.** `POST /v1/wallet/credit` is not part of your
  integration.
* **`GET /v1/wallet` reports zero** for your VIPs, because there is no member
  wallet in play. That is expected, not a bug - the cleanest setup is simply not to
  grant `WALLET_READ` on your key.
* **No per-redemption webhook fires.** There is no wallet movement to report, so
  neither `wallet.credited` nor `wallet.debited` is emitted. Poll `GET /v1/balance`,
  or subscribe to `org.points.low`, to track the pool.
* **Analytics still work.** `GET /v1/analytics/offers` and
  `/v1/analytics/timeseries` report pool-settled spend exactly as they report
  wallet-settled spend.

## Reading your balance

`GET /v1/balance` is server-side only: it needs an Org-API-Key with the
`BILLING_READ` scope. An organization with no pool yet returns zeros rather than an
error.

<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.",
    );
  }

  const expys = initialize({
    baseUrl: process.env.EXPYS_BASE_URL,
    environment: "sandbox",
    token: orgApiKey,
  });

  async function main(): Promise<void> {
    const account = await expys.balance();

    console.log(`settlement mode: ${account.settlementMode}`);
    console.log(`balance: ${account.balance} points`);

    if (account.settlementMode === "ORG_POOL") {
      // Spendable headroom includes the credit limit: a postpaid org may overdraw
      // to -creditLimit before redemptions are refused with INSUFFICIENT_ORG_POINTS.
      const spendable = account.balance + account.creditLimit;
      console.log(`spendable now: ${spendable} points`);
      console.log(`lifetime spent from the pool: ${account.lifetimeSpent}`);

      if (spendable <= 0) {
        console.warn(
          "Pool exhausted - redemptions will be refused until topped up.",
        );
      }
    } else {
      // MEMBER_WALLET: this balance funds the points you credit to VIPs, and each
      // VIP's own wallet is what a redemption debits.
      console.log(
        "VIP redemptions debit each member's wallet, not this balance.",
      );
    }
  }
  ```

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

  @main
  struct BalanceExample {
    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 client = ExpysClient(
        configuration: ExpysConfiguration(token: orgApiKey, environment: .sandbox, baseURL: baseURL)
      )

      let account = try await client.balance()

      print("settlement mode: \(account.settlementMode.rawValue)")
      print("balance: \(account.balance) points")

      switch account.settlementMode {
      case .orgPool:
        // Spendable headroom includes the credit limit: a postpaid org may overdraw
        // to -creditLimit before redemptions are refused with INSUFFICIENT_ORG_POINTS.
        let spendable = account.balance + account.creditLimit
        print("spendable now: \(spendable) points")
        print("lifetime spent from the pool: \(account.lifetimeSpent)")

        if spendable <= 0 {
          print("Pool exhausted - redemptions will be refused until topped up.")
        }
      case .memberWallet:
        // memberWallet: this balance funds the points you credit to VIPs, and each
        // VIP's own wallet is what a redemption debits.
        print("VIP redemptions debit each member's wallet, not this balance.")
      }
    }
  }
  ```

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

  import com.expys.sdk.ExpysClient
  import com.expys.sdk.ExpysConfiguration
  import com.expys.sdk.ExpysEnvironment
  import com.expys.sdk.models.GetBalanceResponse
  import kotlinx.coroutines.runBlocking

  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 client = ExpysClient.create(
      ExpysConfiguration(
        token = orgApiKey,
        environment = ExpysEnvironment.SANDBOX,
        baseUrl = System.getenv("EXPYS_BASE_URL") ?: ExpysConfiguration.DEFAULT_BASE_URL,
      ),
    )

    val account = client.balance()

    println("settlement mode: ${account.settlementMode.value}")
    println("balance: ${account.balance} points")

    when (account.settlementMode) {
      GetBalanceResponse.SettlementMode.ORG_POOL -> {
        // Spendable headroom includes the credit limit: a postpaid org may overdraw
        // to -creditLimit before redemptions are refused with INSUFFICIENT_ORG_POINTS.
        val spendable = account.balance + account.creditLimit
        println("spendable now: $spendable points")
        println("lifetime spent from the pool: ${account.lifetimeSpent}")

        if (spendable <= 0) {
          println("Pool exhausted - redemptions will be refused until topped up.")
        }
      }
      GetBalanceResponse.SettlementMode.MEMBER_WALLET -> {
        // MEMBER_WALLET: this balance funds the points you credit to VIPs, and each
        // VIP's own wallet is what a redemption debits.
        println("VIP redemptions debit each member's wallet, not this balance.")
      }
    }
  }
  ```
</CodeGroup>

| Field            | Meaning                                                                |
| ---------------- | ---------------------------------------------------------------------- |
| `balance`        | Points available now. Can be negative if you are postpaid (see below). |
| `creditLimit`    | How far below zero the balance may go. `0` means prepaid.              |
| `lifetimeSpent`  | Points spent straight from the pool by org-pool redemptions.           |
| `settlementMode` | `MEMBER_WALLET` or `ORG_POOL`.                                         |

## Prepaid and postpaid

Both are the same mechanism, controlled by `creditLimit`:

* **Prepaid** (`creditLimit: 0`, the default): the pool can never go negative. The
  moment it cannot cover a redemption, that redemption is refused.
* **Postpaid** (`creditLimit > 0`): you may overdraw to `-creditLimit`, and are
  invoiced for the negative balance at period close. A redemption that would take
  you past the limit is still refused.

Spendable headroom is therefore `balance + creditLimit`.

## Funding the pool

<CardGroup cols={2}>
  <Card title="Enterprise grant" icon="building">
    For a signed contract paid by invoice or wire, our team grants your pool an
    allotment directly - no card needed. Ask your Expys contact.
  </Card>

  <Card title="Self-serve purchase" icon="credit-card">
    Buy points any time from the portal Billing page. You are taken to Stripe to
    pay; your balance updates as soon as payment is confirmed.
  </Card>
</CardGroup>

Your balance and full grant / purchase / spend history are on the **Billing** page
of the [developer portal](https://app.expys.com).

## Webhooks

Subscribe to keep your systems in step with the pool:

| Event                  | Fires when                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `org.points.low`       | The pool crosses to or below its low-water mark (once per crossing, in either mode). |
| `org.points.purchased` | A self-serve top-up has credited your pool.                                          |

See [Webhooks](/guides/webhooks) for delivery, signing, and retries.

## Handling an empty pool

```json theme={null}
{
  "error": {
    "code": "INSUFFICIENT_ORG_POINTS",
    "message": "Insufficient organization points"
  }
}
```

This `402` surfaces on `POST /v1/wallet/credit` in member-wallet mode, and on
`POST /v1/redemptions` in org-pool mode. Either way it is all-or-nothing: no points
move, and in org-pool mode **no booking is created**. Top the pool up (grant or
purchase) and retry.

<Note>
  In member-wallet mode, an organization with no pool at all is ungated -
  distributions are free until your first top-up sets the pool up. Org-pool mode is
  never ungated: without a funded pool, every redemption is refused.
</Note>
