Skip to main content
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

FieldDescription
statusHTTP status code (for example 409).
codeA stable, machine-readable string (the contract - branch on this, not on message).
messageA human-readable description. May change; never branch on it.
retryAfterMsPresent on rate-limit errors, parsed from Retry-After.
requestIdThe X-Request-Id of the failed call. Quote it to support.
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.

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.
StatusKindTypeScriptCommon codes
401unauthorizedUnauthorizedErrorUNAUTHORIZED
403forbiddenForbiddenErrorFORBIDDEN
404notFoundNotFoundErrorNOT_FOUND
409conflictConflictErrorREDEMPTION_ALREADY_EXISTS, IDEMPOTENCY_KEY_REUSED
422validationValidationErrorINSUFFICIENT_POINTS, OFFER_UNAVAILABLE, VALIDATION
429rateLimitedRateLimitErrorRATE_LIMITED
5xxserverApiErrorINTERNAL
Beyond HTTP, the SDKs also surface transport-level kinds that never reached the server: network, timeout, decoding, and notConfigured.

Handling errors

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

Stable codes worth handling

These code values are stable and worth branching on explicitly:
CodeStatusMeaning
REDEMPTION_ALREADY_EXISTS409The member already redeemed this offer.
IDEMPOTENCY_KEY_REUSED409An idempotency key was reused with a different request body.
INSUFFICIENT_POINTS422The member’s balance is below the offer’s pointsPrice.
OFFER_UNAVAILABLE422The offer is expired or not redeemable.
RATE_LIMITED429Too many requests; honor retryAfterMs.
WEBHOOK_ENDPOINT_LIMIT422The org’s webhook-endpoint limit was reached.
WEBHOOK_EVENT_UNKNOWN422A subscribed event name is not in the catalog.
WEBHOOK_URL_NOT_ALLOWED422The 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.
Retryable failures (429 and 5xx) are retried automatically by the SDK with full-jitter backoff. See Retries and idempotency.