Metered APIs raise one question above all: when something goes wrong, who pays? Onekey's answer is that you pay for answers. This post walks through a real sequence of calls and the balance after each, so the rules are not just a table.
The price list is short#
Every endpoint has a flat price, shown on its page and returned by GET /v1/platforms. Endpoints cost 1 to 8 credits, and most cost 1 or 5: a simple lookup such as a profile is 1, a search or a list is 5. There is no per-field pricing and no surprise multiplier. Credits never expire.
Two routes are always free: GET /v1/account (your balance) and GET /v1/platforms (the catalog). That is what makes the experiment below possible: you can read the balance before and after any call without changing it.
import os, requests
API = "https://api.onekey.dev"
H = {"x-api-key": os.environ["ONEKEY_KEY"]}
def balance():
return requests.get(f"{API}/v1/account", headers=H, timeout=30).json()["balance_credits"]
def call(path, **params):
before = balance()
r = requests.get(API + path, params=params, headers=H, timeout=30)
body = r.json()
after = balance()
charged = (body.get("metadata") or {}).get("credits_charged")
err = (body.get("error") or {}).get("type")
print(f"{r.status_code} {err or 'ok':<16} credits_charged={charged} balance {before} -> {after}")
return body
call("/v1/github/profile", username="torvalds")
call("/v1/github/profile", username="onekey-no-such-user-zz9")
call("/v1/github/profile")Run it twice and the first row changes: the second time the profile is served from cache, so credits_charged is 0 and the balance does not move. The error rows print None for credits_charged because a failed response carries no charge.
A real run#
These are real calls made on 2026-09-30, in order, with the balance read before and after each. The first three are the calls in the script above; the last two came from the same session. The balance before the first call was 504.658.
| Call | Result | Balance after | Charged |
|---|---|---|---|
github/profile?username=torvalds | 200, fresh fetch | 503.658 | 1 |
github/profile?username=onekey-no-such-user-zz9 | 404 NOT_FOUND | 503.658 | 0 |
github/profile (no username) | 422 INVALID_PARAMS, "Missing required parameter 'username'" | 503.658 | 0 |
github/repo?owner=torvalds&repo=linux | 200, fresh fetch | 502.658 | 1 |
| the same repo call again | 200, cached: true | 502.658 | 0 |
Read down the last column. Success pays. A record that does not exist does not. A malformed request does not. A repeat inside the fresh window does not.
The rules behind those rows#
| Situation | Cost |
|---|---|
| Fresh fetch that succeeds | The endpoint price. |
| Answer served from cache (fresh or stale) | Free. A cached response over 100 KB costs 0.2 credit, never more than the endpoint price. |
Record not found (404) | Free: reserved, then refunded. |
Source failed or timed out (502) | Free: refunded. |
Invalid parameters (422) | Free: rejected before any charge. |
Bad key (401), rate limited (429) | Free. |
| An empty list from a search that succeeded | The endpoint price. It is a real answer. |
| Each further page of a list | The endpoint price per page. |
A replay for an Idempotency-Key you already used | Free. |
The empty-search row surprises people, so it is worth saying why. When a search returns nothing, the source was asked, answered, and the answer was "nothing matches". That is information, and it cost real work to get. A 404 on a single record is different: the record you named does not exist, so there is no answer to pay for. Refunds for empty search results are on the docs' coming-soon list, not in effect.
How "reserved, then refunded" works#
Before a call runs, your balance is checked and the price is reserved. If you cannot afford it, you get 402 INSUFFICIENT_CREDITS and nothing happens. If the call ends without data (404, 502), the reservation is released and you are back where you started. If it ends with data, the reservation becomes the charge. That is why the 404 row above shows the same balance before and after: nothing was taken, then nothing was given back.
Two consequences for your code:
- Retrying a failed call is safe. A
502was refunded, so a retry costs nothing extra. Back off, and stop after a few tries. If a platform keeps returning502, the free status endpoint tells you whether it is down. - You do not need to reconcile.
metadata.credits_chargedon every response is what that call cost, andX-Credits-Remainingcarries the new balance after any call that charged something. It is omitted on free cache hits and replays, because nothing was charged and the balance was not re-read.
If your code might repeat a call after a timeout, add an Idempotency-Key: a repeat within 24 hours is replayed free even when the first attempt succeeded and its response was lost on the way back.
Cache hits are the big lever#
The last row of the table is the reason to think about freshness. The same lookup a hundred times inside its fresh window is one credit and ninety-nine free cache hits. Most endpoints share one cache across customers, and simultaneous identical requests are merged into one fetch. If you need the age of what you got, read metadata.age_s; if metadata.stale is true, the copy was past its fresh window and a refresh is running in the background. The behaviour is on the caching page.
Fencing off a script#
An account has one balance, but a key can carry its own credit limit. A key that reaches its limit gets 402 KEY_BUDGET_EXCEEDED on the next call it cannot afford, while every other key keeps working. The check is atomic with the balance check, so concurrent requests cannot both squeeze under it, and refunds are given back to the key. Use one key per app and a limit on the risky ones. See per-key credit limits.
Batches and lists#
Compound calls follow the same rule row by row. A batch of 100 lookups charges only the rows that came back 200, each at its own endpoint's price; failed and not-found rows are refunded. A paged list charges per page (see why every list uses one cursor shape), so cap the number of pages you fetch.
The short version#
- Success is charged at the endpoint's flat price. Failure and rejection are not.
- Repeats inside the fresh window are free.
- The response tells you what it cost, and the balance is free to read.
- Anything that might repeat should carry an idempotency key.
The reference is credits and pricing, the prices are on the pricing page, and the terms are in the glossary: credit, refund and cache hit.