API Reference

Limits

What stops a request, where each stop is visible before it bites, and what to do with the two refusals it can produce.

More keys do not raise anything

Balance and ceilings belong to the account, not to the key. Every key you issue draws on the same balance and the same ceilings, so a second key divides what you have rather than adding to it. Capacity is shared the same way: another key, or another account, does not buy more of it.

What limits a request

Three things can stop a request, and each produces exactly one status. There is no requests-per-minute limit and no per-key quota.

Kinds of limit
LimitWhat it governsError on exceedingWhere to check
BalanceHow much the account can spend in total, plus the reserve held while a request runs.402 payment_requiredGET /api/v1/key → limit_remaining
Spending ceilingsA daily and a monthly cap in dollars on the whole account, shared by all its keys.402 payment_requiredGET /api/v1/key → limit, limit_reset
CapacityHow much of a model can be served at once, across every customer.503 provider_overloadedthe Retry-After header on that response

Scroll the table horizontally for the error and where to check it.

Checking what is left

One route answers both money questions: GET https://api.gtokens.io/api/v1/key, with the same bearer header as every other call.

shell
curl https://api.gtokens.io/api/v1/key \
  -H "Authorization: Bearer sk-sl-your-key"
main.py
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"]

# What this account can still spend right now, in dollars.
print(key["limit_remaining"], "left;", key["usage_daily"], "spent today")
main.ts
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()

// Stop the queue before requests start coming back 402.
if (data.limit_remaining < 1) console.warn('almost out:', data.limit_remaining)

It costs nothing and needs no balance, so it is safe to call before a batch and between runs. Poll it on a schedule rather than per request — the figures move only when a request settles.

What the key route returns

TypeScript
type KeyInfo = {
  data: {
    label: string          // the prefix of the key that made the call, e.g. "sk-sl-a1b2c3d4..."

    usage: number          // charged to this account, all time, in dollars
    usage_daily: number    // ... in the current day
    usage_weekly: number   // ... in the current week, from Monday
    usage_monthly: number  // ... in the current month

    limit: number | null   // the account spending ceiling, or null when none is set
    limit_reset: 'daily' | 'monthly' | null   // which window that ceiling counts over
    limit_remaining: number                   // the smaller of the balance and what is
                                              // left under the ceiling; never null

    is_free_tier: boolean  // always false: every request is paid from the balance
  }
}
limit describes the key you called with

limit is this key’s own ceiling and limit_reset says which window it counts over — daily, weekly, monthly, or null for a ceiling that covers the whole life of the key. A key without a ceiling of its own falls back to the account ceiling, which is what these two fields described before key limits existed.

limit_remaining 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. That is why it can be smaller than limit minus usage — another ceiling is closer. It is always a number, never null.

The four usage counters are calendar windows in the account’s own time zone: the day starts at midnight, the week on Monday, the month on the first. The zone is the one the dashboard reports from the browser; an account that has never opened it stays on UTC. The ceilings are enforced over those same windows, so limit_remaining and the refusal agree with each other.

What the credits route returns

GET /api/v1/credits is the shorter answer: two all-time figures whose difference is the balance.

TypeScript
type Credits = {
  data: {
    total_credits: number  // credited to the account, all time, in dollars
    total_usage: number    // charged for requests, all time, in dollars
  }
}

// total_credits - total_usage is the current balance, always.

Neither figure resets or rolls off, so a long-running process can subtract them and stop itself at zero without keeping state of its own. Per-request charges are in your log in the account area; the counters here are the totals behind it.

Spending limits

Money stops a request from three places, and all three answer with the same status:

  1. The balance. It is prepaid, and a request is served while it covers the request. When it does not, the answer is 402 payment_required and nothing is charged — no answer, no charge. Read it as limit_remaining, or as total_credits - total_usage.
  2. The account ceilings. A daily and a monthly cap in dollars on the whole account, counted over calendar windows in the account’s own time zone — the day from midnight, the month from the first — across every key. They are a safety catch against a key that got loose, not a cap on your work: at a ceiling the answer is the same 402 until the window turns over. A new account has none; write to us with the amounts you want.
  3. The ceiling on the key itself. Each key can carry its own cap and its own window — daily, weekly, monthly, or one that covers the whole life of the key — set on the API keys screen in your account area. It is the ceiling you give a key that leaves your hands; the other keys keep working when it is reached. This is the one limit and limit_reset describe on GET /api/v1/key, and limit_remaining is the smallest of all three numbers above.

The reserve held while a request runs

What a request will cost is not known until it finishes, so an estimate is held from the balance before it goes out and settled against the real counters when it comes back. The difference is returned; a request that fails returns the whole reserve.

For text the estimate is your max_tokens — 4,096 when you do not set it — plus the input, at that model’s rates. For images it is the price of the frames you asked for, at the size you asked for. The practical consequence is worth knowing: a thin balance with a very large max_tokens can be refused even though the answer would have been three words long. Ask for the ceiling you actually need.

Handling 402

  • Top up. Add funds from the balance page in your account area and send the request again; nothing else about the account changes, and your keys stay yours.
  • Or wait out the window. If a ceiling was reached rather than the balance emptied, the same request goes through once the window has rolled on — limit_reset says which window that is.
  • Watch it before it bites. Call GET /api/v1/key on a schedule and act on limit_remaining: a queue that stops itself at a threshold beats one that discovers the limit request by request.

Balance and top-ups

The balance is prepaid. You top it up from the balance page in your account area, which shows the methods and the details, and lists every top-up with its state.

  • No minimum, one fee. Add what you like. A service fee of 5% of the credited amount is added on top and shown before you pay: credit $50.00, send $52.50.
  • The balance does not expire. No shelf life, no inactivity fee. Unused balance is refunded on request, within up to 10 business days — write to support@gtokens.io from your account email. If we shut the service down, we say so and refund every unused balance on the same terms.
  • Nothing is charged for a refusal. A request that comes back 402 or 503 costs nothing at all. The one case that does cost is a streamed answer that started and then broke: it is billed for what the model produced.
If a top-up has not appeared

Crediting happens when the balance page checks for your payment — press Sync now after you have paid. It can take a few minutes to appear, and the page shows the state of every top-up. If one is still missing, write to us with the details from that page rather than sending it again.

Open the balance page

Capacity

There is no cap on how often you may call us: no requests-per-minute limit, per key or per account. What is finite is the pool that serves a model. While it has room your requests go straight through; when it is fully busy the next one is refused.

A refusal is the honest answer here, and it is deliberate. The model you named is the model that answers, or you get a status you can act on — a busy pool never quietly hands your request to something else. Work is spread evenly across the pool, and which part of it served you is not observable and not meant to be: the model, the response shape and the charge are identical either way.

response body
{
  "error": {
    "code": 503,
    "message": "no provider is available for this model right now",
    "metadata": {
      "error_type": "provider_overloaded",
      "reason": "no_provider_available"
    }
  }
}

The response carries a Retry-After header, always — computed from the state of the pool where that is known, and a small default otherwise. The request is not charged.

Handling 503

  • Honour the interval. Wait what Retry-After says and send the same request again. The number is not a constant; coming back earlier mostly fails again and adds load to a pool that is already full.
  • Back off if it repeats. On a second and third refusal, grow the wait rather than looping at the same interval — the queue ahead of you is draining at its own pace.
  • Do not switch models. A 503 says this model has no room this second, not that it is the wrong model. Another model is another pool, another price and another answer, and swapping on a transient refusal is how a batch ends up half generated by something you did not choose.

When it happens after the stream has started

On a streamed call the status is sent with the first byte, so a refusal that arrives later cannot be one. It comes as an event in the stream instead, and the stream ends after it:

stream
event: error
data: {"type":"error","error":{"type":"overloaded_error","message":"provider is temporarily unavailable, retry later","error_type":"provider_overloaded"},"request_id":"gen-4f9c2a1b7d3e5061","metadata":{"reason":"no_provider_available"}}

Everything that can be decided before the first byte still arrives as a real status, so this is the narrow case of a request that had already begun. Treat an error event as terminal.