Management API

Onlist ships an OpenRouter-compatible management surface under /api/v1/. It lets you create and revoke API keys, read your balance, look up what a single call cost, and authorize a third-party application — all without opening a browser.

Two kinds of credential

The credential model mirrors OpenRouter's, and the split is the point:

CredentialCan make inference callsCan manage keys, credits, activity
API key (sk-...)YesNo — except reading its own record
Management key (mgmt_...)NoYes

A management key cannot make inference requests. This is not a policy check that could be forgotten somewhere: management keys live in a different table from API keys, so /v1/chat/completions simply does not find them and answers 401. A leaked management key cannot spend your balance directly — though it can create an API key that does, so treat it as a high-value secret.

Conversely, an API key sent to /api/v1/keys or /api/v1/credits gets 403. It can only read its own record (GET /api/v1/key) and its own calls (GET /api/v1/generation).

Creating a management key

Go to Management keys in your account menu and create one. Creation and revocation require a passkey or 2FA confirmation, because a management key can list, modify and delete every API key on the account.

The key is shown once, right after it is created. Onlist stores only a hash, so there is no "show it again" — if you lose it, revoke it and create another. You can hold up to 20 active management keys, each with an optional expiry.

Caution

Everything under /api/v1/ is served from https://onlist.io only. The China acceleration endpoint (https://api.onlist.net) relays inference traffic and answers 403 for these paths. Keep your inference client wherever it performs best and point management calls at the main domain.

Response format

Success responses wrap the payload in data:

{ "data": { "total_credits": 25.0, "total_usage": 3.42 } }

Errors use the OpenRouter envelope, where code is the HTTP status:

{ "error": { "code": 403, "message": "Only management keys can perform this operation" } }

This differs from the inference surface (/v1/...), which uses the OpenAI error shape. Both are stable; pick the parser that matches the path you are calling.

Rate limits on this surface are 60 requests per minute per credential and 500 per day per account. Exceeding either returns 429.

Credits

curl -sS https://onlist.io/api/v1/credits \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY"
{ "data": { "total_credits": 25.0, "total_usage": 3.42 } }

total_credits is everything you have ever added; total_usage is everything you have spent. Your current balance is the difference. Both are plain USD, the same unit as the prices on every model page.

Keys

List

curl -sS "https://onlist.io/api/v1/keys?offset=0" \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY"

Returns up to 100 keys per page, newest first. Add include_disabled=true to include disabled keys; by default only enabled ones are listed.

A key object looks like this:

{
  "hash": "4821",
  "name": "ci",
  "label": "abcd**********wxyz",
  "disabled": false,
  "limit": 5.0,
  "limit_remaining": 4.37,
  "limit_reset": "daily",
  "include_byok_in_limit": false,
  "usage": 12.84,
  "usage_daily": 0.63,
  "usage_weekly": 4.11,
  "usage_monthly": 12.84,
  "byok_usage": 0,
  "byok_usage_daily": 0,
  "byok_usage_weekly": 0,
  "byok_usage_monthly": 0,
  "created_at": "2026-08-01T09:14:00Z",
  "updated_at": null,
  "expires_at": null,
  "external_user": null,
  "creator_user_id": null,
  "workspace_id": "default"
}
  • hash is the opaque identifier you pass back in /api/v1/keys/{hash}. On Onlist it happens to be a decimal number rather than a hash string; treat it as opaque.
  • label is the masked key. The full key is only ever shown at creation time.
  • usage is the key's lifetime spend. usage_daily / usage_weekly / usage_monthly are computed from your usage log over the current UTC day, ISO week (from Monday) and calendar month.
  • limit_remaining comes from the live budget counter, which includes in-flight reservations, so it can differ from limit - usage_* for a moment during a burst.
  • byok_*, external_user, creator_user_id and workspace_id are present for compatibility and carry no meaning on Onlist.

Create

curl -sS -X POST https://onlist.io/api/v1/keys \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "ci", "limit": 5, "limit_reset": "daily" }'
{
  "key": "sk-...",
  "data": { "hash": "4821", "name": "ci", "limit": 5.0, "limit_reset": "daily", "...": "..." }
}

The plaintext key appears in the top-level key field, once. Store it before you discard the response.

name is required and capped at 50 characters. limit is a USD amount; limit_reset selects the window:

limit_resetMeaning
omitted / nullTotal cap — spend it once, then the key stops
"daily"Resets at 00:00 UTC
"weekly"Resets every 7 days
"monthly"Rejected with 400 — see below
Important

monthly is rejected, not approximated Onlist has no calendar-month spend window, and mapping monthly onto the 7-day window would let a key spend roughly four times what you asked for while reporting the limit you set. A limit on money is not something to approximate, so the request fails loudly instead.

Important

limit must be greater than zero "limit": 0 is rejected with 400. Internally a zero limit is the "no limit" sentinel, so storing it would turn "this key may not spend anything" into "this key may spend without a cap" — the most dangerous direction to get wrong. The same applies to amounts too small to represent (below $0.000002). To stop a key from spending, use "disabled": true or delete it.

Note

limit_reset: "weekly" uses Onlist's rolling 7-day budget window, which is aligned to Unix epoch multiples — that is, Thursday 00:00 UTC. OpenRouter resets weekly limits on Monday. The usage_weekly reporting field does use ISO weeks (Monday), because it is a log aggregate rather than a billing counter.

Read, update, delete

# Read
curl -sS https://onlist.io/api/v1/keys/4821 \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY"

# Rename and disable
curl -sS -X PATCH https://onlist.io/api/v1/keys/4821 \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "ci (paused)", "disabled": true }'

# Clear the spend limit
curl -sS -X PATCH https://onlist.io/api/v1/keys/4821 \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "limit": null }'

# Delete
curl -sS -X DELETE https://onlist.io/api/v1/keys/4821 \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY"

PATCH is a partial update and limit has three states: omit the field to leave the limit alone, send null to clear it, send a number to set it. Sending limit without limit_reset keeps the key's current window. Sending limit_reset without limit does the reverse — it moves the existing amount to the new window, so you can switch a $5 daily cap to a $5 weekly cap without restating the amount. (A key that has no limit yet is unchanged: a window with no amount attached to it means nothing.)

DELETE returns { "data": { "deleted": true } }. Deleting a key is immediate and permanent; requests using it start failing with 401.

Reading your own key

An API key can look itself up. This is the one management endpoint an ordinary key can call:

curl -sS https://onlist.io/api/v1/key \
  -H "Authorization: Bearer YOUR_ONLIST_API_KEY"

It returns the same key object plus is_free_tier, is_management_key, is_provisioning_key and a rate_limit object.

Note

rate_limit is a compatibility placeholder OpenRouter deprecated this field, and Onlist does not apply per-key request-rate limits on the inference surface at all — only spend budgets. Rather than invent a number that an SDK might use for client-side throttling, Onlist returns { "requests": -1, "interval": "", "note": "deprecated" }.

A management key calling this endpoint gets its own record instead, with is_management_key: true, no limit and no usage.

What one call cost

Every Onlist response carries an X-Oneapi-Request-Id header. Feed that value back to /api/v1/generation:

curl -sS "https://onlist.io/api/v1/generation?id=THE_REQUEST_ID" \
  -H "Authorization: Bearer YOUR_ONLIST_API_KEY"
{
  "data": {
    "id": "THE_REQUEST_ID",
    "model": "qwen/qwen3-max",
    "provider_name": "Example Shop",
    "streamed": true,
    "latency": 420,
    "generation_time": 2000,
    "created_at": "2026-09-07T06:11:03Z",
    "tokens_prompt": 120,
    "tokens_completion": 34,
    "native_tokens_prompt": 120,
    "native_tokens_completion": 34,
    "native_tokens_cached": 16,
    "total_cost": 0.5,
    "usage": 0.5,
    "finish_reason": "stop",
    "native_finish_reason": "stop",
    "is_byok": false
  }
}

total_cost and usage are the same USD amount you were charged. latency is time-to-first-token in milliseconds and is null for non-streaming calls, which never measure it. generation_time is the total wall time in milliseconds.

Fields Onlist does not record (upstream_id, http_referer, user_agent, origin, api_type, cache_discount, native_tokens_reasoning) are returned as null rather than omitted — null means "no data", which is a different statement from 0.

An API key can only look up its own calls. A management key can look up any call on the account.

Note

/api/v1/generation takes the value of the X-Oneapi-Request-Id header — the same value that appears as request_id in GET /api/log/token. It is not X-Onlist-Route-Id, which identifies the routing decision rather than the log entry.

Activity

curl -sS https://onlist.io/api/v1/activity \
  -H "Authorization: Bearer mgmt_YOUR_MANAGEMENT_KEY"

Returns one row per (UTC day × model × provider) over the last 30 complete UTC days:

{
  "data": [
    {
      "date": "2026-09-06",
      "model": "qwen/qwen3-max",
      "model_permaslug": "qwen/qwen3-max",
      "endpoint_id": "example-shop/qwen/qwen3-max",
      "provider_name": "Example Shop",
      "usage": 1.0,
      "byok_usage_inference": 0,
      "requests": 2,
      "prompt_tokens": 300,
      "completion_tokens": 50,
      "reasoning_tokens": 0
    }
  ]
}

Today is deliberately excluded: it is still accumulating, and a reconciliation endpoint whose answer changes over the course of a day is not much use.

Optional filters: date=YYYY-MM-DD for a single day (must fall inside the window, or you get 400), and api_key_hash=<hash> for a single key. A hash that is not yours returns an empty array rather than an error — it is a filter, not a resource lookup. user_id, group_by and workspace_id are accepted and ignored.

Sign in with Onlist (OAuth PKCE)

If you are building an application that needs an Onlist key for its users, you do not need to ask them to paste one. Send them to Onlist's consent page and receive a key they authorized.

The flow is standard OAuth PKCE:

  1. Generate a code_verifier (43–128 characters) and its S256 challenge.
  2. Open https://onlist.io/auth?callback_url=...&code_challenge=...&code_challenge_method=S256 in the user's browser.
  3. The user reviews what they are granting — optionally setting a spend cap — and confirms. Onlist redirects to your callback_url with ?code=....
  4. Exchange the code for a key.
# 1. Verifier and challenge
v=$(openssl rand -hex 32)
c=$(printf %s "$v" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')

# 2-3. Send the user to:
#   https://onlist.io/auth?callback_url=http://localhost:9999/cb&code_challenge=$c&code_challenge_method=S256

# 4. Exchange the code from the callback
curl -sS -X POST https://onlist.io/api/v1/auth/keys \
  -H "Content-Type: application/json" \
  -d "{ \"code\": \"THE_CODE\", \"code_verifier\": \"$v\" }"
{ "key": "sk-...", "user_id": null }

This exchange endpoint takes no credentials — your application does not have any at this point, which is exactly the gap PKCE closes. Only the client that started the authorization knows the verifier.

Rules worth knowing before you integrate:

  • The code is valid for 10 minutes and can be redeemed once. A wrong verifier burns it too: you have to send the user through the flow again. Retrying a failed verifier would be a brute-force window.
  • A wrong code and a wrong verifier return the identical 403. Distinguishing them would turn the endpoint into a code-existence oracle.
  • callback_url must be https, or http on the user's own machine (localhost, 127.0.0.1, [::1], *.localhost, any port). The code travels in the URL query, so plaintext HTTP to a public host would hand it to anyone in the middle.
  • Omit callback_url for headless tools: the consent page shows the code for the user to copy.
  • user_id in the response is always null. OpenRouter uses it for an application's own user identifier; Onlist has no such concept and will not put an internal account id there.

Next steps

  • SDKs: The Python and TypeScript wrappers for everything on this page, including a PKCE helper.
  • Authentication: Bearer tokens, header variants, and attribution headers.
  • Usage & Cost: The usage object, how cost is metered, and the dashboard view.
  • Migrate from OpenRouter: Endpoint-by-endpoint comparison.