Skip to main content
The SDKs make transient failures and accidental double-submits safe by default: retryable responses are retried with backoff, and writes carry an idempotency key so repeating a request never acts twice.

Automatic retries

The SDKs retry only on responses that are safe to retry: HTTP 429 and 5xx. Every other failure - including 4xx other than 429 - is surfaced to your code immediately, with no retry. See Errors for the typed error shape and the codes worth branching on. Retries use full-jitter exponential backoff: a base delay of 500ms, a cap of 10s, and a random delay drawn between zero and the current ceiling on each attempt. When the response carries a Retry-After header, it is honored instead and clamped to the range [0, 300s]. By default the SDKs allow maxRetries: 2 - up to 3 total attempts - and apply no per-request timeout. Both are configurable; see Configuration.

Backoff illustration

With the defaults (base 500ms, cap 10s, maxRetries: 2), a request that keeps receiving 429/5xx is delayed within these windows before each retry. The actual delay is a uniform random pick inside the window (full jitter):
AttemptRetryDelay window
1initial requestnone
2first retry0 - 500ms
3second retry0 - 1000ms
The ceiling doubles each time (500ms, 1s, 2s, …) until it reaches the 10s cap, then stays there. A Retry-After header, when present, overrides the computed window for that attempt.
Reads (GET) are inherently safe to retry. Writes are made safe to retry by the idempotency key the SDK attaches automatically - so the built-in retries never double-charge or double-redeem.

Idempotency

Every write accepts an Idempotency-Key header, a UUIDv4. The SDKs generate one automatically for each write; you may also supply your own - persist it alongside the operation so a retry after a crash reuses the same key and the server replays the original response instead of acting again. Reusing a key with a different request body is a conflict: the API returns 409 IDEMPOTENCY_KEY_REUSED. These endpoints support idempotency:
EndpointOperation
POST /v1/auth/exchangeExchange credentials for a member token.
POST /v1/redemptionsRedeem an offer.
POST /v1/wallet/creditCredit points to a wallet.
POST /v1/webhooksRegister a webhook endpoint.
import {
  ConflictError,
  generateIdempotencyKey,
  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> {
  const offer = process.env.EXPYS_OFFER_ID ?? "off_1";

  // Generate the key once and persist it (e.g. with the operation record) so a
  // retry after a crash reuses it; the server replays the original response.
  const idempotencyKey = generateIdempotencyKey();
  console.log(`idempotency key: ${idempotencyKey}`);

  // The same key sent twice replays the first result rather than acting twice.
  for (const attempt of [1, 2]) {
    try {
      const redemption = await expys.createRedemption(
        { offer },
        { idempotencyKey },
      );
      console.log(
        `attempt ${attempt}: redemption ${redemption.id} [${redemption.status}]`,
      );
    } catch (error) {
      if (error instanceof ConflictError) {
        console.log(`attempt ${attempt}: conflict (${error.code})`);
        continue;
      }
      throw error;
    }
  }
}
curl
curl -X POST https://api.expys.com/v1/redemptions \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN" \
  -H "Idempotency-Key: 3f1b2c44-0a9e-4d3a-9b2f-1e6a7c8d9e0f" \
  -H "Content-Type: application/json" \
  -d '{"offer":"off_1"}'

Idempotency on {id} routes

The OpenAPI emitter drops the Idempotency-Key header from the API reference for routes with a path parameter - for example POST /v1/conversations/{id}/messages. This is a documentation-only limitation: idempotency is still fully supported on those routes. The SDKs continue to send the header automatically, and when you call such an endpoint directly you should set the header yourself.
Absence of Idempotency-Key from the reference for an {id} route does not mean the route ignores it. Send the header; reusing it with a different body still returns 409 IDEMPOTENCY_KEY_REUSED.

Errors

The typed error taxonomy and the stable codes worth handling.

Configuration

Tune maxRetries, timeouts, and the rest of the client options.