Docs
View as Markdown

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.
shell
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:

shell
# 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#

SituationAnswerCharged
First call succeeds200, stored for 24 hoursThe endpoint price (or free if it was a cache hit)
Same request again200 replay: X-Idempotent-Replay: true, credits_charged: 0Nothing
First call still running409 IDEMPOTENCY_IN_PROGRESS with Retry-After: 1Nothing; retry after one second and you get the replay
Different request, same key422 IDEMPOTENCY_KEY_REUSEDNothing
Key is not a UUID400 IDEMPOTENCY_KEY_INVALIDNothing
Response was over 64 KB409 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 failedThe 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 unreachable503 IDEMPOTENCY_STORE_UNAVAILABLE with Retry-After: 1: refused before billing rather than risk billing twice.Nothing
Request died mid-wayThe key stays claimed for at most 5 minutes, then is free again.Nothing; any reservation is refunded
shell
# 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 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.