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

# Idempotency and rate limits

> Safe retries with the Idempotency-Key header, and the per-organization request quota.

## Idempotency

A request might time out before you read the response. You don't know if it ran. Replaying it could create a duplicate payout, beneficiary, or quote.

The `Idempotency-Key` header solves this. Pass a unique value on every write request. We cache the response for 24 hours keyed by `(organization, key, method, path)`. A retry within that window returns the cached response. The handler does not run twice.

### How to use it

Generate a unique key per operation (UUID v4 is fine) and send it on any non-GET request:

```bash theme={null}
curl -X POST https://api.nxos.io/v1/quotes \
  -H "Authorization: Bearer nxos_sk_test_..." \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "acct_...",
    "fromAsset": "USD",
    "toAsset": "USDC",
    "fromAmount": "100.00"
  }'
```

The header is optional. If you omit it, the request runs as usual with no replay protection.

### Outcomes

| Scenario                                             | Outcome                                                                                                     |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| First request with a fresh key                       | Handler runs. Response is cached and returned.                                                              |
| Retry, same key, same body, response cached          | Cached response is returned. The `Idempotent-Replayed: true` header is set so you can detect the cache hit. |
| Retry, same key, same body, original still in flight | `409 idempotency_request_in_flight`. Wait briefly and retry.                                                |
| Retry, same key, different body                      | `409 idempotency_key_in_use`. You reused the key for a different operation. Generate a new key.             |
| Original returned `5xx`                              | Not cached. The next attempt re-acquires the key and runs the handler.                                      |

### Caching policy

We cache `2xx` and `4xx` responses. Same key + same body always returns the same response.

We do not cache `5xx`. Server errors are usually transient. Your retry should hit a healthy server, not a stale cached failure.

### Key requirements

* Maximum 255 characters
* Unique per logical operation (UUID v4 recommended)
* Reuse the same key for retries of the same operation. Do not reuse it for a different operation.

### Scope

Keys are scoped per organization, per HTTP method, per request path. The same key value is independent across `POST /v1/quotes` and `POST /v1/transactions/crypto-payouts`. They are different operations.

GET endpoints ignore the header.

## Rate limits

Every API endpoint is rate-limited per organization. The default cap is **1,000 requests per minute**, applied as a tumbling 60-second window.

### Per organization, not per key

Limits apply to your organization. Creating more API keys does not raise your quota. Your throughput is one number across all your keys.

### Response headers

Every successful response (and every `429`) includes these headers so you can pace yourself before hitting the limit:

| Header                  | Value                                            |
| ----------------------- | ------------------------------------------------ |
| `X-RateLimit-Limit`     | The cap configured for your organization.        |
| `X-RateLimit-Remaining` | Requests left in the current window.             |
| `X-RateLimit-Reset`     | Unix timestamp (seconds) when the window resets. |

```http theme={null}
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 873
X-RateLimit-Reset: 1714323600
Content-Type: application/json
```

### When you exceed the limit

Requests beyond the cap return `429 rate_limited` with a `Retry-After` header indicating seconds until the window resets:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714323623
Content-Type: application/json

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded: 1000 requests per minute. Retry in 23 seconds.",
    "requestId": "req_abc..."
  }
}
```

The next window admits you immediately. Wait for `Retry-After` rather than retrying right away.

### Handling 429 in your client

```javascript theme={null}
async function callWithBackoff(url, init, maxAttempts = 3) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(url, init);
    if (response.status !== 429) return response;

    const retryAfter = Number(response.headers.get('Retry-After') ?? '1');
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
  }
  throw new Error('Rate limited after retries');
}
```

### IP-based protection

A separate edge rule (CloudFront WAF) caps requests per source IP for abuse and DDoS protection. That layer is independent of the per-organization quota above. Most partners never see it. If you do, contact support.

## How they interact

A `429` response is not cached. Your next attempt with the same `Idempotency-Key` re-runs against the rate-limited handler. Once the window resets, the request is admitted normally.

A cached replay (`Idempotent-Replayed: true`) does count against your rate limit. The cached response is still a request you sent us. If you see high replay volume, your client is sending the same request more often than it needs to. Check the client logic.

<Info>
  Send an `Idempotency-Key` on every write request. Honor `Retry-After` on `429`. That covers the two most common ways an integration breaks.
</Info>
