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

# Retries and idempotency

> Automatic retries with full-jitter backoff on 429/5xx, and idempotency keys that make writes safe to repeat.

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](/guides/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](/guides/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):

| Attempt | Retry           | Delay window |
| ------- | --------------- | ------------ |
| 1       | initial request | none         |
| 2       | first retry     | 0 - 500ms    |
| 3       | second retry    | 0 - 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.

<Tip>
  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.
</Tip>

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

| Endpoint                 | Operation                                |
| ------------------------ | ---------------------------------------- |
| `POST /v1/auth/exchange` | Exchange credentials for a member token. |
| `POST /v1/redemptions`   | Redeem an offer.                         |
| `POST /v1/wallet/credit` | Credit points to a wallet.               |
| `POST /v1/webhooks`      | Register a webhook endpoint.             |

<CodeGroup>
  ```typescript TypeScript theme={null}
  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;
      }
    }
  }
  ```

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

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

      // 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.
      let idempotencyKey = generateIdempotencyKey()
      print("idempotency key: \(idempotencyKey)")

      // The same key sent twice replays the first result rather than acting twice.
      for attempt in 1...2 {
        do {
          let redemption = try await client.createRedemption(
            .init(offer: offer),
            idempotencyKey: idempotencyKey
          )
          print("attempt \(attempt): redemption \(redemption.id) [\(redemption.status)]")
        } catch ExpysError.api(let error) where error.kind == .conflict {
          print("attempt \(attempt): conflict (\(error.code))")
        }
      }
    }
  }
  ```

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

  import com.expys.sdk.ExpysClient
  import com.expys.sdk.ExpysConfiguration
  import com.expys.sdk.generateIdempotencyKey
  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,
      ),
    )

    val offer = client.listOffers(limit = 1).`data`.firstOrNull() ?: return@runBlocking

    // Persist this key; reusing it on a retry replays the original response.
    val key = generateIdempotencyKey()
    val first = client.createRedemption(CreateRedemptionRequest(offer = offer.id), idempotencyKey = key)
    val replay = client.createRedemption(CreateRedemptionRequest(offer = offer.id), idempotencyKey = key)

    println("created ${first.id}; replay returned ${replay.id}")
  }
  ```
</CodeGroup>

```bash curl theme={null}
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.

<Note>
  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`.
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/guides/errors">
    The typed error taxonomy and the stable codes worth handling.
  </Card>

  <Card title="Configuration" icon="sliders" href="/guides/configuration">
    Tune `maxRetries`, timeouts, and the rest of the client options.
  </Card>
</CardGroup>
