Idempotency

POST /v1/swaps is designed for safe retry. Sending the same Idempotency-Key with the same request body returns the original swap — your retry won't create a second one.

How it works

We hash the credential id, the idempotency key, and the request body, and cache the response for 24 hours. On a repeat call:

ScenarioBehavior
Same key + same bodyReturns the original cached response (HTTP 201 with the original swap.id)
Same key + different bodyReturns HTTP 409 conflict (code: idempotency_key_mismatch) — protects against accidental key reuse with mismatched data
New keyCreates a fresh swap

After 24 hours the cache entry expires; reusing the key after that creates a new swap.

Two layers of protection

We persist both the cached response and a hash of the request body on the swap row itself. If the response cache write briefly fails after the upstream swap was created (rare but possible), the next retry still finds the swap row, validates the body hash, and returns the original swap (or a 409 if the body changed). You should not see duplicate swaps from a network blip.

When to use

Always, on every POST /v1/swaps call. There is no good reason to omit it. Generate a UUID v4 per logical attempt — once per "user clicked Confirm Swap", not once per HTTP retry.

Choosing a key

A UUID v4 (randomUUID() in Node, uuid.uuid4() in Python) is the right default. The key must be:

  • Unique across attempts that are logically distinct.
  • Stable across HTTP retries of the same attempt.

If your service crashes after generating the key but before storing it, that's fine — the next retry will get the cached response on success, or will create the swap fresh on first failure. Either way, exactly one swap exists.

Examples

Node.js

import { randomUUID } from 'node:crypto';
 
async function createSwap(input) {
  const idempotencyKey = randomUUID();
  // Persist `idempotencyKey` alongside your order before calling the API.
  await orderStore.update(input.orderId, { idempotencyKey });
 
  const res = await fetch(`${BASE}/v1/swaps`, {
    method: 'POST',
    headers: {
      'Authorization': AUTH,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify(input),
  });
  return res.json();
}

Retry on transient failures

async function createSwapWithRetry(input, attempts = 3) {
  const idempotencyKey = randomUUID();
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch(`${BASE}/v1/swaps`, {
        method: 'POST',
        headers: {
          'Authorization': AUTH,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey, // SAME key across retries
        },
        body: JSON.stringify(input),
      });
      if (res.status >= 500) throw new Error(`Server error ${res.status}`);
      return res.json();
    } catch (err) {
      if (i === attempts - 1) throw err;
      await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
    }
  }
}

The same key across retries means you create at most one swap, regardless of how many network errors you hit.

What's NOT idempotent

Read endpoints (GET /v1/*) are inherently safe to retry — no idempotency key needed.

POST /v1/quotes is not currently keyed. Quotes are cheap and stateless; you can re-quote freely.

POST /v1/addresses/validate is also not keyed.

In v1, only POST /v1/swaps requires Idempotency-Key.

See also

  • Errorsconflict envelope details.
  • Rate limits — what happens when you retry too fast.