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. |
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.
| 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
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:
| 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.
Retryable failures (429 and 5xx) are retried automatically by the SDK with
full-jitter backoff. See Retries and idempotency.