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

# Idempotency

> Make safe retries without creating duplicate transactions

## Why Idempotency Matters

In payment systems, network failures and timeouts are inevitable. Without idempotency, retrying a failed request could create a duplicate transaction — charging a user twice or triggering two payouts. Idempotency keys let you safely retry requests with the guarantee that the operation only executes once.

## The `Idempotency-Key` Header

Include an `Idempotency-Key` header on mutating requests. The key uniquely identifies the intent of the request — same key, same result.

```bash theme={null}
curl -X POST https://api.stablepay.global/v2/transactions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "type": "sell",
    "userId": "usr_abc123",
    "amount": "100",
    "asset": "USDT",
    "network": "polygon"
  }'
```

| Endpoint                           | Idempotency Key                                    |
| ---------------------------------- | -------------------------------------------------- |
| `POST /v2/transactions`            | **Required** — request will be rejected without it |
| `POST /v2/transactions/:id/payout` | Recommended                                        |
| Other POST endpoints               | Recommended                                        |

## Key Format

Use a **UUID v4** for each unique operation:

```javascript theme={null}
const idempotencyKey = crypto.randomUUID();
// → "550e8400-e29b-41d4-a716-446655440000"
```

```python theme={null}
import uuid
idempotency_key = str(uuid.uuid4())
```

<Warning>
  Generate a new key for each distinct operation. Do not reuse a key from a previous transaction for a new one — this will return the cached response from the original transaction.
</Warning>

## Key Expiry

Keys expire after **24 hours**. After expiry, the same key can be used for a new request.

## Behavior

| Scenario                    | HTTP Status | Response                                                                       |
| --------------------------- | ----------- | ------------------------------------------------------------------------------ |
| New unique key              | `201`       | Transaction created                                                            |
| Same key, same body (retry) | `200`       | Cached response from original request, with `Idempotent-Replayed: true` header |
| Same key, different body    | `409`       | Conflict — no action taken                                                     |

### Successful Retry

When you retry with the same key and body, the response includes a header indicating it's a replay:

```
HTTP/1.1 200 OK
Idempotent-Replayed: true
```

The response body is identical to the original `201` response.

### Conflict (Different Body)

If you accidentally reuse a key with different parameters:

```json theme={null}
{
  "error": "Idempotency Conflict",
  "message": "This idempotency key was already used with a different request body",
  "code": "IDEMPOTENCY_CONFLICT"
}
```

Generate a new key and retry.

## Retry Example

```javascript theme={null}
async function createTransactionWithRetry(payload, maxRetries = 3) {
  const idempotencyKey = crypto.randomUUID();

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('https://api.stablepay.global/v2/transactions', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey
        },
        body: JSON.stringify(payload)
      });

      if (response.status === 429) {
        const retryAfter = (await response.json()).retryAfter ?? 60;
        await new Promise(r => setTimeout(r, retryAfter * 1000));
        continue;
      }

      return await response.json();
    } catch (err) {
      if (attempt === maxRetries) throw err;
      await new Promise(r => setTimeout(r, 1000 * attempt));
    }
  }
}
```

The key is generated **once** outside the loop. Every retry sends the same key, so even if a previous attempt succeeded but the response was lost, the retry returns the original result without creating a duplicate.

## Recovering a Lost Transaction ID

If your client crashes after sending a request but before saving the `transactionId`, you can recover it by replaying the same idempotency key and body:

```bash theme={null}
# Replay the exact same request with the same key
curl -X POST https://api.stablepay.global/v2/transactions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{ "type": "sell", "userId": "usr_abc123", "amount": "100", "asset": "USDT", "network": "polygon" }'
```

If the original request succeeded, you'll get back the original response (including the `transactionId`) with the `Idempotent-Replayed: true` header. This works as long as the key hasn't expired (24 hours).

<Note>
  Store your idempotency keys alongside the operation they represent. This makes recovery straightforward — if in doubt, just replay the request.
</Note>
