# Idempotency

A retry after a timeout or a dropped connection has a problem: you do not know whether the first attempt went through and was billed. Send an `Idempotency-Key` header and it stops mattering. The same key on the same request is billed at most once; a repeat gets the stored answer back for free.

## When to use it

On any billed data call (`GET /v1/{platform}/{endpoint}`) that your code might send more than once: retry loops, queue workers, serverless functions that can be invoked twice. Requests without the header behave exactly as before.

## How it works

1. Generate a UUID (any version) per distinct request and send it as `Idempotency-Key`. The key is scoped to your account: any of your keys sees the same record.
2. The key is claimed **before** anything is billed. If the request completes with `200`, the response is stored under that key.
3. Send the same request with the same key again within **24 hours** and you get the stored response back: same `data`, `metadata.credits_charged: 0`, `metadata.idempotent_replay: true`, a fresh `request_id`, plus the headers `X-Idempotent-Replay: true` and `X-Cache: HIT`. The rest of `metadata` (`cached`, `age_s`, `fetched_at`) is the original's, so it describes the answer you first got, not the moment of the replay.
4. A failed attempt (any status other than `200`) releases the key. Retrying with the same key runs the request for real and is charged as usual if it succeeds.

```bash
curl -i "https://api.onekey.dev/v1/github/profile?username=torvalds" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Idempotency-Key: 6f1c2d3e-4b5a-4c6d-8e7f-90a1b2c3d4e5"
# HTTP/1.1 200 OK
# x-cache: MISS
# x-credits-remaining: 826.8
```

```json
{
  "success": true,
  "data": { "author": { "username": "torvalds", "...": "..." } },
  "metadata": {
    "platform": "github", "endpoint": "profile",
    "cached": false, "stale": false, "age_s": 0.012, "fetched_at": "2026-09-30T02:52:14.755390+00:00",
    "credits_charged": 1, "request_id": "cd394835d1ca492080b42615bce1df93", "idempotent_replay": false
  },
  "error": null
}
```

The same request again, within 24 hours:

```bash
# HTTP/1.1 200 OK
# x-cache: HIT
# x-idempotent-replay: true
```

```json
{
  "success": true,
  "data": { "author": { "username": "torvalds", "...": "..." } },
  "metadata": {
    "platform": "github", "endpoint": "profile",
    "cached": false, "stale": false, "age_s": 0.012, "fetched_at": "2026-09-30T02:52:14.755390+00:00",
    "credits_charged": 0, "request_id": "69aee091df4b49deb46abd20222d6055", "idempotent_replay": true
  },
  "error": null
}
```

## Every outcome

| Situation | Answer | Charged |
| --- | --- | --- |
| First call succeeds | `200`, stored for 24 hours | The endpoint price (or free if it was a cache hit) |
| Same request again | `200` replay: `X-Idempotent-Replay: true`, `credits_charged: 0` | Nothing |
| First call still running | `409 IDEMPOTENCY_IN_PROGRESS` with `Retry-After: 1` | Nothing; retry after one second and you get the replay |
| Different request, same key | `422 IDEMPOTENCY_KEY_REUSED` | Nothing |
| Key is not a UUID | `400 IDEMPOTENCY_KEY_INVALID` | Nothing |
| Response was over 64 KB | `409 IDEMPOTENCY_REPLAY_UNAVAILABLE`: the original call was billed and returned, but not stored. Fetch again with a new key (a new billed call). | Nothing for this answer |
| First call failed | The failure (`404`, `502`, `429`...) and the key is released; a later retry with the same key runs for real. | Nothing (failed calls are not charged) |
| Store unreachable | `503 IDEMPOTENCY_STORE_UNAVAILABLE` with `Retry-After: 1`: refused before billing rather than risk billing twice. | Nothing |
| Request died mid-way | The key stays claimed for at most 5 minutes, then is free again. | Nothing; any reservation is refunded |

```bash
# same key, other username
# HTTP/1.1 422 Unprocessable Content
```

```json
{
  "success": false,
  "data": null,
  "metadata": { "request_id": "c6193f7cf31b4895aae1483345e1d48f" },
  "error": {
    "code": "idempotency_key_reused",
    "type": "IDEMPOTENCY_KEY_REUSED",
    "message": "This Idempotency-Key was already used for a different request",
    "request_id": "c6193f7cf31b4895aae1483345e1d48f",
    "doc_url": "https://onekey.dev/errors/IDEMPOTENCY_KEY_REUSED"
  }
}
```

## Edge cases

- Responses larger than 64 KB are returned normally the first time but are not stored, so a replay answers `409 IDEMPOTENCY_REPLAY_UNAVAILABLE` instead of silently re-billing. If your list calls come close to that size, keep the answer on your side.
- A replay still counts as a request for [rate limiting](https://onekey.dev/docs/rate-limits.md) and carries the `X-RateLimit-*` headers.
- A replay does not carry `X-Credits-Remaining`: nothing was charged, so the balance is not re-read.
- The key is claimed atomically, so two identical requests sent at the same moment cannot both be billed: one runs, the other gets `409 IDEMPOTENCY_IN_PROGRESS`.
- A `404` is not stored either: the key is released, and the next attempt with it is a real lookup.

## What it costs

Nothing extra. The first successful call costs the endpoint price as usual; every replay within 24 hours is free and shows `credits_charged: 0`. The `409`, `422`, `400` and `503` answers above are never charged.
