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

# Offers

> List the catalog of offers a member can redeem, with points pricing, expiry, and cursor pagination.

Offers are the catalog of experiences a member can redeem. `listOffers` returns
the offers available to the calling member, each with its points price and
optional expiry, paged with a cursor.

<Note>
  These are **member-mode** calls: they use the member token, not the
  Org-API-Key. See [Authentication](/authentication). Whether you read the
  sandbox demo catalog or your live catalog is selected by the key the token was
  minted from - see [Environments](/environments).
</Note>

## List offers

`listOffers(limit?, cursor?)` calls `GET /v1/offers` and returns an `OfferList`.
The snippet below pages through the entire catalog by following `nextCursor`
until it comes back `null`:

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

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

  async function main(): Promise<void> {
    let cursor: string | undefined;
    let page = 0;
    let total = 0;

    // Loop until the server returns a null nextCursor, marking the end of the list.
    do {
      const result = await expys.listOffers({ cursor, limit: 50 });
      page += 1;
      total += result.data.length;
      console.log(`page ${page}: ${result.data.length} offers`);
      cursor = result.nextCursor ?? undefined;
    } while (cursor);

    console.log(`done: ${total} offers across ${page} page(s)`);
  }
  ```

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

  @main
  struct PaginationExample {
    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)
      )

      var cursor: String?
      var page = 0
      var total = 0

      // Loop until the server returns a nil nextCursor, marking the end of the list.
      repeat {
        let result = try await client.listOffers(limit: 50, cursor: cursor)
        page += 1
        total += result.data.count
        print("page \(page): \(result.data.count) offers")
        cursor = result.nextCursor
      } while cursor != nil

      print("done: \(total) offers across \(page) page(s)")
    }
  }
  ```

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

  import com.expys.sdk.ExpysClient
  import com.expys.sdk.ExpysConfiguration
  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,
        baseUrl = System.getenv("EXPYS_BASE_URL") ?: ExpysConfiguration.DEFAULT_BASE_URL,
      ),
    )

    var cursor: String? = null
    var total = 0
    do {
      val page = client.listOffers(limit = 50, cursor = cursor)
      total += page.`data`.size
      page.`data`.forEach { println("- ${it.title} (${it.id})") }
      cursor = page.nextCursor
    } while (cursor != null)

    println("fetched $total offers across all pages")
  }
  ```
</CodeGroup>

```bash curl theme={null}
curl https://api.expys.com/v1/offers?limit=50 \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN"
```

## The Offer schema

Each entry in `data` is an `Offer`:

| Field              | Type            | Description                                                                        |
| ------------------ | --------------- | ---------------------------------------------------------------------------------- |
| `id`               | string          | Stable offer id. Pass it as `offer` to [create a redemption](/guides/redemptions). |
| `title`            | string          | Display title of the offer.                                                        |
| `description`      | string          | Full description.                                                                  |
| `shortDescription` | string          | A condensed description for list views and cards.                                  |
| `kind`             | string          | The offer kind.                                                                    |
| `type`             | string          | The offer type.                                                                    |
| `pointsPrice`      | integer \| null | Points debited on redemption. `null` means no points cost (free).                  |
| `expiresAt`        | string \| null  | ISO-8601 expiry. `null` means the offer does not expire.                           |

<Note>
  `pointsPrice` of `null` is distinct from `0`: it means the offer carries no
  points price at all. An offer past its `expiresAt` is no longer redeemable -
  attempting to redeem it returns `OFFER_UNAVAILABLE`. See
  [Redemptions](/guides/redemptions).
</Note>

## Pagination

`OfferList` is cursor-paginated:

| Field        | Type           | Description                                                                  |
| ------------ | -------------- | ---------------------------------------------------------------------------- |
| `data`       | Offer\[]       | The offers on this page.                                                     |
| `nextCursor` | string \| null | Pass back as `cursor` to fetch the next page. `null` when there are no more. |

| Parameter | Type    | Description                                                      |
| --------- | ------- | ---------------------------------------------------------------- |
| `limit`   | integer | Maximum number of offers to return on a page.                    |
| `cursor`  | string  | The `nextCursor` from the previous page. Omit on the first call. |

<Note>
  Pagination is cursor-based, not page-numbered. Pass the previous response's
  `nextCursor` as the next request's `cursor`, and stop when `nextCursor` is
  `null`. Do not assume a fixed page size or construct cursors yourself - treat
  the cursor as an opaque token.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Redemptions" icon="ticket" href="/guides/redemptions">
    Redeem an offer, spend points, and follow the redemption lifecycle.
  </Card>

  <Card title="Points and wallet" icon="coins" href="/guides/points-and-wallet">
    How points are minted, spent, and tracked against `pointsPrice`.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    The full `GET /v1/offers` schema and an interactive playground.
  </Card>
</CardGroup>
