Skip to main content
V2 write endpoints that produce irreversible side effects (creating vaults, 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

RouteAnchorAnchor sourced fromTTL
POST /v2/enrollflowIdStage-1 POST /v2/enroll/signature response24 hours
POST /v2/portfolios/{id}/withdrawmessage.nonceStage-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.
{
  "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).
{
  "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.
  • Reserves a pooled KMS agent slot and peeks the user’s next account index.
  • Do not generate fresh flowIds to retry stage 2. Each new flowId costs a round trip through stage 1 and may burn an 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. The server uses it as both the replay key and the uniqueness guarantee for the onchain transfer.

Worked Example — Safe Retry Loop

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.