Give the key its own ceiling on the API keys screen: an amount in dollars and a reset period — every day, every week, every month, or for the lifetime of the key. GET / then reports it as limit and limit_, and limit_ says what can be spent right now. Once the ceiling is reached, that key gets 402 payment_, nothing is charged, and every other key keeps working.
- The windows are calendar ones in the account’s time zone: the day from midnight, the week from Monday, the month from the first — not the last 24 hours.
limit_is the smallest of the balance, what is left of the key’s ceiling and what is left of the account’s daily and monthly ones. Read it; do not work it out fromremaining limitminususage.- A ceiling bounds what a key can spend. It does not set money aside: all keys share one balance.
Why put a ceiling on each key
An account can have as many keys as it needs, and the usual advice is one per project or machine. They all draw on the same prepaid balance, and each can be revoked on its own. A key without a ceiling of its own can spend whatever that balance holds.
A ceiling on the key turns that into an amount you chose. It suits a key that runs where nobody is watching it closely — a scheduled job, a staging environment, a service on a machine of its own: a loop that goes wrong, or a key that leaks, stops at the ceiling, and the other keys keep working. The keys screen shows what each key has been charged, so a ceiling can be set from what the key actually uses.
The balance belongs to the account, not to the key. A second key divides what you have rather than adding to it, and a ceiling on one key frees nothing for the others.
Set it on the keys screen
Ceilings on keys are set on the API keys screen of your account area. New key opens the issue dialog, and its Key limit group holds the two fields that make one decision:
- Credit limit — the amount in dollars. The key stops working once it has been charged this much in the period below. Optional: leave it blank for no limit.
- Reset the limit — Every day, Every week, Every month, or For the lifetime of the key. A period needs an amount; without one there is nothing to reset.
For a key that already exists, choose Edit in the actions menu of its row, or open the key and press Edit under Settings. To take a ceiling off, use Remove limit in the same dialog: emptying the amount field does not remove it. The key’s page shows the ceiling, its period and what is Left in this period; the list shows the same figure in its limit column.
The account can carry a daily and a monthly ceiling of its own, across every key. A new account has none; to have them set, write to us with the amounts you want. Key ceilings and account ceilings both apply, and whichever is lower decides.
Reset periods and how the windows are counted
The period you pick is what limit_ reports, and each one has a counter of the same window on GET /. All four count what this key has been charged:
| Reset the limit | limit_reset | The window | Counter on this key |
|---|---|---|---|
| Every day | daily | From midnight of the current day | usage_ |
| Every week | weekly | From midnight on Monday | usage_ |
| Every month | monthly | From midnight on the first of the month | usage_ |
| For the lifetime of the key | null | No window: it never resets | usage |
Scroll the table sideways for the window and its counter.
The windows are calendar windows in the account’s own time zone: the day starts at midnight, the week on Monday, the month on the first, and each returns to zero at its own boundary. The zone is the time zone your browser reports when you open the dashboard; until then, UTC. The ceilings are enforced over those same windows, so limit_ and the refusal agree with each other.
An hour after midnight usage_ reports one hour of work, not yesterday evening plus one hour. If you need a trailing figure, keep it yourself: these counters cannot produce one.
Read what is left from the key route
GET / describes the key in the header: there are no parameters, it needs no balance and the call is not charged, so it is safe to run on a schedule or before a job.
import requests
r = requests.get(
"https://api.gtokens.io/api/v1/key",
headers={"Authorization": "Bearer sk-sl-your-key"},
timeout=30,
)
r.raise_for_status()
key = r.json()["data"]
if key["limit"] is None:
print("no ceiling: the prepaid balance is the only bound")
else:
window = key["limit_reset"] or "lifetime of the key"
print(f"ceiling {key['limit']:.2f} USD, window: {window}")
# The one number to act on: what the next request can spend.
print(f"can spend now: {key['limit_remaining']:.2f} USD")
print(f"this key: today {key['usage_daily']:.2f}, week {key['usage_weekly']:.2f}, "
f"month {key['usage_monthly']:.2f}, all time {key['usage']:.2f}")type KeyInfo = {
label: string
usage: number
usage_daily: number
usage_weekly: number
usage_monthly: number
limit: number | null
limit_remaining: number
limit_reset: 'daily' | 'weekly' | 'monthly' | null
is_free_tier: boolean
}
const r = await fetch('https://api.gtokens.io/api/v1/key', {
headers: { Authorization: 'Bearer sk-sl-your-key' },
})
if (!r.ok) throw new Error(`key check failed: ${r.status}`)
const { data } = (await r.json()) as { data: KeyInfo }
console.log(
data.limit === null
? 'no ceiling: the prepaid balance is the only bound'
: `ceiling ${data.limit} USD, window: ${data.limit_reset ?? 'lifetime of the key'}`,
)
// The one number to act on: what the next request can spend.
console.log(`can spend now: ${data.limit_remaining.toFixed(2)} USD`)A key with a daily ceiling, a little over four dollars into its day, answers like this:
{
"data": {
"label": "sk-sl-ab12…",
"usage": 412.905311,
"usage_daily": 4.10322,
"usage_weekly": 31.884004,
"usage_monthly": 128.443121,
"limit": 100.0,
"limit_remaining": 95.89678,
"limit_reset": "daily",
"is_free_tier": false
}
}limit is this key’s own ceiling, or the account ceiling when the key has none — and null when there is neither, with the balance as the only bound. The usage counters are what this key has been charged, built from finished requests: a call still in flight is not in them yet.
limit_ answers one question — how much can be spent right now — and it is the smallest of everything that can stop the next request: the balance, what is left of this key’s ceiling, and what is left of the account’s daily and monthly ones. It is always a number, never null. It can be smaller than limit minus usage, and then another ceiling is closer; on an account whose balance runs out first it is the balance, moving with every top-up.
A request in flight has already put its estimated cost aside against the balance and against every ceiling it falls under, so limit_ drops when the call starts, while the usage counters move only when it finishes and the exact amount is known. Every key on the account draws on the same balance, so the figure you read can be spent by another key a moment later.
Guard a batch before it starts
A batch that starts with too little left does not stop politely: it stops in the middle, with part of the work done and paid for and the rest coming back refused. Reading limit_ first turns that into a decision made in advance. The per-item figure is yours to measure — usage. on a reply is the exact amount charged for it.
import requests
API = "https://api.gtokens.io/api/v1"
HEADERS = {"Authorization": "Bearer sk-sl-your-key"}
def can_spend() -> float:
r = requests.get(f"{API}/key", headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()["data"]["limit_remaining"]
def run_batch(prompts: list[str], per_item_usd: float) -> list[str]:
"""per_item_usd is your own figure: usage.cost of a trial request, plus headroom."""
needed = len(prompts) * per_item_usd
left = can_spend()
if left < needed:
raise SystemExit(f"{left:.2f} USD left, the batch needs about {needed:.2f}")
answers = []
for prompt in prompts:
r = requests.post(
f"{API}/chat/completions",
headers=HEADERS,
json={
"model": "anthropic/claude-haiku-4-5",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 512, # the hold is sized by this: ask for what you need
},
timeout=300, # a request can wait minutes for a busy model before it starts
)
if r.status_code == 402:
# A ceiling or the balance. Sending again now gets the same answer.
message = r.json()["error"]["message"]
print(f"stopped after {len(answers)} of {len(prompts)}: {message}")
break
r.raise_for_status()
answers.append(r.json()["choices"][0]["message"]["content"])
return answersconst API = 'https://api.gtokens.io/api/v1'
const headers = {
Authorization: 'Bearer sk-sl-your-key',
'Content-Type': 'application/json',
}
async function canSpend(): Promise<number> {
const r = await fetch(`${API}/key`, { headers })
if (!r.ok) throw new Error(`key check failed: ${r.status}`)
const { data } = await r.json()
return data.limit_remaining
}
// perItemUsd is your own figure: usage.cost of a trial request, plus headroom.
export async function runBatch(prompts: string[], perItemUsd: number): Promise<string[]> {
const needed = prompts.length * perItemUsd
const left = await canSpend()
if (left < needed) {
throw new Error(`${left.toFixed(2)} USD left, the batch needs about ${needed.toFixed(2)}`)
}
const answers: string[] = []
for (const prompt of prompts) {
const r = await fetch(`${API}/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'anthropic/claude-haiku-4-5',
messages: [{ role: 'user', content: prompt }],
max_tokens: 512, // the hold is sized by this: ask for what you need
}),
})
if (r.status === 402) {
// A ceiling or the balance. Sending again now gets the same answer.
const { error } = await r.json()
console.warn(`stopped after ${answers.length} of ${prompts.length}: ${error.message}`)
break
}
if (!r.ok) throw new Error(`request failed: ${r.status}`)
const body = await r.json()
answers.push(body.choices[0].message.content)
}
return answers
}Two details in the loop matter. The estimate held while a request runs is sized by max_ — 4,096 when you do not set it — so asking for what you need keeps a thin remainder from refusing a short answer. And the loop still handles 402: the check before the run is a plan, not a guarantee, because another key on the account can spend the same money first.
When the ceiling is reached
A request the key’s ceiling cannot cover comes back 402 with error_ before anything is generated, so it costs nothing. Requests still in flight count against the ceiling with their estimate, so a burst of parallel calls stops at the ceiling rather than all passing it. An empty balance and an account ceiling answer with the same status; the message is what tells them apart. What to do depends on which one it was:
| What stopped it | Who else is stopped | What to do |
|---|---|---|
| The key’s own ceiling | Only this key. | Wait for the window that limit_ names to turn over, or raise the ceiling on the keys screen. A lifetime ceiling has no window to wait out. |
| An account ceiling | Every key on the account. | Wait for the day or the month to turn over, or write to us to change the amounts. |
| The balance | Every key on the account. | Top up from the balance page, then send the request again. |
Scroll the table sideways for what to do.
Sending the same request again before anything changes gets the same 402, so a batch should stop on it rather than retry. The errors reference lists every status and its envelope.



