> ## 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 conflict responses for B2B API write endpoints.

B2B API write routes use explicit idempotency for operations with permanent
effects. Examples include smart-account creation and onchain transfers.

Most routes use an anchor that Glider returns in stage 1. You send the same
anchor in stage 2. Chain activation uses a hash of the signed payload.
An identical replay returns the cached response.

## Idempotent routes

| Route                                    | Anchor                                             | Source                                                | Retry-record TTL |
| ---------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | ---------------- |
| `POST /v2/enroll`                        | `flowId`                                           | Stage-1 `POST /v2/enroll/signature` response          | 24 hours         |
| `POST /v2/portfolios/{id}/chains`        | Hash of `portfolioId`, `chainIds`, and `signature` | Derived by the server from the signed stage-2 payload | 24 hours         |
| `POST /v2/portfolios/{id}/withdraw`      | `message.nonce`                                    | Stage-1 withdrawal response and `authorizationId`     | 24 hours         |
| `POST /v2/portfolios/{id}/liquidate-all` | `message.nonce`                                    | Stage-1 liquidate-all response and `authorizationId`  | 24 hours         |

Glider stores an enrollment record with the tenant and the API key. Withdrawal
and chain-activation services also check other keys for the same tenant.
Therefore, a different API key does not make a duplicate operation safe.

<Warning>
  The withdrawal authorization and the retry record have different lifetimes.
  Send each stage-2 request before the 10-minute `message.expiresAt`. Glider
  checks this expiry before it reads the idempotency cache.

  Glider keeps the retry record for 24 hours. This record prevents nonce reuse
  and request conflicts. It does not extend the authorization.
</Warning>

## Replay results

An anchor that Glider has already received can have three results:

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

The body matches the first operation. Glider returns the original response and
does not repeat the effect. Enrollment and chain activation can replay during
their 24-hour retry period.

Withdrawal and liquidate-all retries must also arrive before the signed
authorization expires.

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

The first request is still in progress. Wait and retry the same request.

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

Wait 1, 2, 4, and then 8 seconds between retries. Do not wait more than 30
seconds between retries. Most requests finish in a few seconds.

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

The anchor matches, but the body is different. Glider rejects the replay
because the requests can produce different results. Do one of these actions:

* Send the original body verbatim.
* Restart the applicable two-stage flow. Do not edit or reuse an old signed
  authorization for a different operation.

```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`). Wait and retry with the same
  anchor and body.
* **`API_007`**. In-progress replay. Back off and retry.
* **Network errors before a response.** Wait and retry with the same anchor.
  If Glider processed the first request, the retry returns the cached result.

### Do not retry

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

### Consult before retrying

* **`404`**. A retry with the same ID returns the same error. Check the tenant
  and the resource ID.
* **`401`/`403`**. Fix auth or scope first.

## Anchor details

### `flowId` (enrollment)

* Issued by `POST /v2/enroll/signature` as an opaque string.
* Valid for 24 hours. After that time, stage 2 returns `400`.
* Binds the assigned agent wallet and peeks the user's next account index.
* **Do not create a new `flowId` to retry stage 2.** Use the original
  `flowId` unless the error is final.

### `message.nonce` (withdrawal)

* A 32-byte `0x`-prefixed hex string issued by
  `POST /v2/portfolios/{id}/withdraw/signature`.
* Also returned as `authorizationId` in the response envelope. The two fields
  have the same value.
* The signed authorization is valid for 10 minutes from issuance. After that,
  any stage-2 submission, including an otherwise identical replay, returns
  `API_216`.
* Glider keeps the retry record for 24 hours. This record does not override
  the signed expiry.
* The nonce is part of the EIP-712 hash. Thus, the signature binds the nonce
  to the withdrawal. The nonce is also the replay key for the transfer.

The liquidate-all flow uses the same nonce, expiry, and retry machinery.

### Signed payload hash (chain activation)

* Stage 1 is deterministic and does not reserve an idempotency record.
* Stage 2 derives its key from `portfolioId`, the ordered `chainIds`, and the
  owner's `signature`.
* Retry with the same `chainIds` and signature. A different signed payload is
  a different activation request.
* Completed and in-flight records live for 24 hours.

## Safe retry example

```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");
}
```

Reuse `message` and `signature` unchanged for each attempt. The nonce in
`message` is the idempotency anchor. Therefore, Glider returns a cached result
for an identical replay.

## Routes without an explicit anchor

* `POST /v2/strategies` creates a new strategy for each call. Your application
  must prevent duplicates.
* `POST /v2/portfolios/{id}/start` and `/stop` are idempotent by state. They do
  not use an explicit anchor.
* `POST /v2/portfolios/{id}/rebalance` has a cooldown for each portfolio. A
  duplicate trigger can return `429` with `Retry-After`.
* Read routes are safe to retry and do not need an anchor.
