> ## Documentation Index
> Fetch the complete documentation index at: https://docs.glider.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry rules, idempotency anchors, and the three 409 sub-codes you may see when calling V2 write endpoints.

V2 write endpoints that produce irreversible side effects (creating smart
accounts, dispatching onchain transfers) are **idempotent on a caller-supplied
anchor**. A replay with the same anchor and the same body returns the cached
response. A replay with the same anchor and a different body is rejected.
This keeps retries safe: you can re-send the exact request without fearing
double-enrollment or double-withdrawal.

## Which Routes Are Idempotent

| Route                               | Anchor          | Anchor sourced from                                                                                 | TTL        |
| ----------------------------------- | --------------- | --------------------------------------------------------------------------------------------------- | ---------- |
| `POST /v2/enroll`                   | `flowId`        | Stage-1 `POST /v2/enroll/signature` response                                                        | 24 hours   |
| `POST /v2/portfolios/{id}/withdraw` | `message.nonce` | Stage-1 `POST /v2/portfolios/{id}/withdraw/signature` response (also surfaced as `authorizationId`) | 10 minutes |

The idempotency key is scoped per tenant and API key. Two different API keys
in the same tenant cannot collide with each other's anchors.

## Replay Semantics

Three outcomes are possible when you re-send a request with an anchor the
server has already seen:

### 1. Cached replay — `200`/`201`/`202`

Same anchor, same body. The original response is returned as if the
operation ran again. No side effects. Safe to retry indefinitely within the
TTL.

### 2. In-progress replay — `409 API_007`

Same anchor, still executing. The first call is in flight and has not yet
committed. Back off and retry.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "API_007",
    "message": "Request with this idempotency key is already being processed"
  }
}
```

Recommended retry: 1s, 2s, 4s, 8s, max 30s. Most in-progress calls resolve
within a few seconds.

### 3. Key conflict — `409 API_008`

Same anchor, **different body**. The server refuses to replay because the
two requests would produce different results. Do not retry with the modified
body. Either:

* Send the original body verbatim.
* Or generate a fresh anchor (for enrollment, re-run stage 1; for withdraw,
  re-run stage 1 — do not reuse the old nonce).

```json theme={null}
{
  "success": false,
  "error": {
    "code": "API_008",
    "message": "Idempotency key conflict: a previous request with this key used a different body"
  }
}
```

## Retry Rules

### Safe to retry

* **`5xx` responses** (`API_600`, `API_506`). Transient server or verifier
  issues. Back off and retry with the same anchor and body.
* **`API_007`**. In-progress replay. Back off and retry.
* **Network errors before a response** (connection reset, timeout). Back off
  and retry with the same anchor. If the server did process the request, the
  retry collapses into a cached replay.

### Do not retry

* **`API_008`**. The server has rejected this body. Start over.
* **`400`-class errors**. The request is wrong. Fix the input before
  retrying.
* **`API_202`** (portfolio already exists). Terminal. The user is already
  enrolled in this strategy.

### Consult before retrying

* **`404`**. The resource was not found. A retry with the same id will hit
  the same 404. Likely a tenant boundary issue.
* **`401`/`403`**. Fix auth or scope first.

## Anchors in Detail

### `flowId` (enrollment)

* Issued by `POST /v2/enroll/signature` as an opaque string.
* Valid for 24 hours from issuance. After that, stage 2 returns `400`.
* Binds the assigned agent wallet and peeks the user's next account index.
* **Do not generate fresh `flowId`s to retry stage 2.** Each new `flowId`
  costs a round trip through stage 1 and may advance the user's account
  index on success. Always retry with the original `flowId` unless you have
  hit a terminal error.

### `message.nonce` (withdrawal)

* A 32-byte `0x`-prefixed hex string issued by
  `POST /v2/portfolios/{id}/withdraw/signature`.
* Also returned as `authorizationId` at the response envelope level — they
  are the same value.
* Valid for 10 minutes from issuance. After that, `API_216` on submit.
* The nonce is part of the EIP-712 hash, so it is already
  cryptographically bound to the authorized withdrawal. It doubles as the
  replay key and the uniqueness guarantee for the onchain transfer.

## Worked Example — Safe Retry Loop

```ts theme={null}
async function submitWithdrawalWithRetry(
  apiKey: string,
  portfolioId: string,
  message: WithdrawalMessage,
  signature: string,
) {
  const maxAttempts = 5;
  let backoffMs = 1000;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch(
      `https://api.glider.fi/v2/portfolios/${portfolioId}/withdraw`,
      {
        method: "POST",
        headers: {
          "x-api-key": apiKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ message, signature }),
      },
    );

    if (res.ok) return await res.json();

    const body = await res.json();
    const code = body?.error?.code;

    if (code === "API_007" || res.status >= 500) {
      await new Promise((r) => setTimeout(r, backoffMs));
      backoffMs = Math.min(backoffMs * 2, 30_000);
      continue;
    }

    throw new Error(`Withdrawal failed: ${code} — ${body?.error?.message}`);
  }

  throw new Error("Withdrawal exceeded max retry attempts");
}
```

The `message` and `signature` are reused verbatim across attempts. The nonce
inside `message` is the idempotency anchor, so repeats collapse into cached
replays on the server.

## What Is Not Idempotent

* `POST /v2/strategies` — strategy creation. Multiple calls produce multiple
  strategies. The caller is responsible for deduping.
* `POST /v2/portfolios/{id}/start` and `/stop` — naturally idempotent by
  state: starting an already-started portfolio is a no-op. Safe to retry,
  but does not use an explicit anchor.
* Read endpoints — always safe to retry, no anchor needed.
