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

# Errors

> One error taxonomy across all three SDKs: typed errors, stable machine-readable codes, and a request id for support.

Every SDK raises the same error taxonomy with the same fields, so error handling
you write once behaves identically in TypeScript, Swift, and Kotlin.

## The error shape

| Field          | Description                                                                          |
| -------------- | ------------------------------------------------------------------------------------ |
| `status`       | HTTP status code (for example `409`).                                                |
| `code`         | A stable, machine-readable string (the contract - branch on this, not on `message`). |
| `message`      | A human-readable description. May change; never branch on it.                        |
| `retryAfterMs` | Present on rate-limit errors, parsed from `Retry-After`.                             |
| `requestId`    | The `X-Request-Id` of the failed call. Quote it to support.                          |

<Note>
  Branch on `code`, never on `message`. The `code` value is part of the API
  contract and is stable across versions; `message` is for humans and can change.
</Note>

## Status to error kind

Each SDK maps the HTTP status to an idiomatic typed error (a subclass in
TypeScript/Swift, a sealed type in Kotlin) so you can catch exactly what you mean.

| Status | Kind           | TypeScript          | Common codes                                             |
| ------ | -------------- | ------------------- | -------------------------------------------------------- |
| 401    | `unauthorized` | `UnauthorizedError` | `UNAUTHORIZED`                                           |
| 403    | `forbidden`    | `ForbiddenError`    | `FORBIDDEN`                                              |
| 404    | `notFound`     | `NotFoundError`     | `NOT_FOUND`                                              |
| 409    | `conflict`     | `ConflictError`     | `REDEMPTION_ALREADY_EXISTS`, `IDEMPOTENCY_KEY_REUSED`    |
| 422    | `validation`   | `ValidationError`   | `INSUFFICIENT_POINTS`, `OFFER_UNAVAILABLE`, `VALIDATION` |
| 429    | `rateLimited`  | `RateLimitError`    | `RATE_LIMITED`                                           |
| 5xx    | `server`       | `ApiError`          | `INTERNAL`                                               |

Beyond HTTP, the SDKs also surface transport-level kinds that never reached the
server: `network`, `timeout`, `decoding`, and `notConfigured`.

## Handling errors

<CodeGroup>
  ```typescript TypeScript theme={null}
  import {
    ApiError,
    ConflictError,
    initialize,
    NetworkError,
    RateLimitError,
    TimeoutError,
    UnauthorizedError,
    ValidationError,
  } 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",
    // A short ceiling so the timeout branch is reachable on a slow network.
    timeoutMs: 10_000,
    token,
  });

  async function main(): Promise<void> {
    try {
      await expys.createRedemption({
        offer: process.env.EXPYS_OFFER_ID ?? "off_1",
      });
      console.log("redemption created");
    } catch (error) {
      // Prefer instanceof on the specific subclass, then refine with the code.
      if (
        error instanceof ConflictError &&
        error.code === "REDEMPTION_ALREADY_EXISTS"
      ) {
        console.log("already redeemed by this member");
      } else if (error instanceof ValidationError) {
        console.log(`validation failed: ${error.code}`);
      } else if (error instanceof UnauthorizedError) {
        console.log(
          "token rejected; re-exchange a fresh member token on your backend",
        );
      } else if (error instanceof RateLimitError) {
        console.log(`rate limited; retry after ${error.retryAfterMs ?? "?"}ms`);
      } else if (error instanceof TimeoutError) {
        console.log("request timed out");
      } else if (error instanceof NetworkError) {
        console.log("network failure; no response received");
      } else if (error instanceof ApiError) {
        // Unknown code: handle as the generic class for its status. Quote requestId.
        console.log(
          `api error ${error.status} (${error.code}) requestId=${error.requestId ?? "n/a"}`,
        );
      } else {
        throw error;
      }
    }
  }
  ```

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

  @main
  struct ErrorHandlingExample {
    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 offer = environment["EXPYS_OFFER_ID"] ?? "off_1"

      let client = ExpysClient(
        configuration: ExpysConfiguration(
          token: token,
          environment: .sandbox,
          baseURL: baseURL,
          // A short ceiling so the timeout branch is reachable on a slow network.
          timeout: 10
        )
      )

      do {
        _ = try await client.createRedemption(.init(offer: offer))
        print("redemption created")
      } catch ExpysError.api(let error) {
        // Switch on the coarse `kind`, then refine with the stable `code`.
        switch error.kind {
        case .conflict where error.code == "REDEMPTION_ALREADY_EXISTS":
          print("already redeemed by this member")
        case .validation:
          print("validation failed: \(error.code)")
        case .unauthorized:
          print("token rejected; re-exchange a fresh member token on your backend")
        case .rateLimited:
          print("rate limited; retry after \(error.retryAfterMs.map(String.init) ?? "?")ms")
        default:
          // Unknown code: handle as the generic class for its kind. Quote requestId.
          print("api error \(error.status) (\(error.code)) requestId=\(error.requestId ?? "n/a")")
        }
      } catch ExpysError.timeout {
        print("request timed out")
      } catch ExpysError.network(let message) {
        print("network failure; no response received: \(message)")
      }
    }
  }
  ```

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

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

    try {
      client.createRedemption(CreateRedemptionRequest(offer = "off_does_not_exist"))
    } catch (error: ExpysException.Api) {
      when (error.error.kind) {
        ApiErrorKind.NOT_FOUND -> println("offer not found (${error.error.code})")
        ApiErrorKind.CONFLICT -> println("already redeemed (${error.error.code})")
        ApiErrorKind.RATE_LIMITED -> println("rate limited; retry after ${error.error.retryAfterMs} ms")
        else -> println("api error ${error.error.status} ${error.error.code}: ${error.error.message}")
      }
      error.error.requestId?.let { println("request id: $it") }
    } catch (error: ExpysException.Timeout) {
      println("request timed out")
    } catch (error: ExpysException.Network) {
      println("network error: ${error.detail}")
    }
  }
  ```
</CodeGroup>

## Stable codes worth handling

These `code` values are stable and worth branching on explicitly:

| Code                        | Status | Meaning                                                      |
| --------------------------- | ------ | ------------------------------------------------------------ |
| `REDEMPTION_ALREADY_EXISTS` | 409    | The member already redeemed this offer.                      |
| `IDEMPOTENCY_KEY_REUSED`    | 409    | An idempotency key was reused with a different request body. |
| `INSUFFICIENT_POINTS`       | 422    | The member's balance is below the offer's `pointsPrice`.     |
| `OFFER_UNAVAILABLE`         | 422    | The offer is expired or not redeemable.                      |
| `RATE_LIMITED`              | 429    | Too many requests; honor `retryAfterMs`.                     |
| `WEBHOOK_ENDPOINT_LIMIT`    | 422    | The org's webhook-endpoint limit was reached.                |
| `WEBHOOK_EVENT_UNKNOWN`     | 422    | A subscribed event name is not in the catalog.               |
| `WEBHOOK_URL_NOT_ALLOWED`   | 422    | The webhook URL failed validation (for example, not HTTPS).  |

## Using the request id

When something goes wrong in production, log `requestId`. It is the
`X-Request-Id` header on the response and lets support trace the exact call in
the server logs. Every response carries one, success or failure.

<Tip>
  Retryable failures (`429` and `5xx`) are retried automatically by the SDK with
  full-jitter backoff. See [Retries and idempotency](/guides/retries-and-idempotency).
</Tip>
