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#
- 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. - The key is claimed before anything is billed. If the request completes with
200, the response is stored under that key. - 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 freshrequest_id, plus the headersX-Idempotent-Replay: trueandX-Cache: HIT. The rest ofmetadata(cached,age_s,fetched_at) is the original's, so it describes the answer you first got, not the moment of the replay. - 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.
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{
"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:
# HTTP/1.1 200 OK
# x-cache: HIT
# x-idempotent-replay: true{
"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 |
# same key, other username
# HTTP/1.1 422 Unprocessable Content{
"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_UNAVAILABLEinstead 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
404is 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.