Accounts & billing
Management keys
Provision, limit, rotate and revoke inference keys from your own code with a management key.
A management key lets your backend administer inference keys through the compatible API: create a key per customer or service, adjust spending limits, disable a leaked key, delete keys you no longer need and read account balances. The endpoints follow OpenRouter's key-management shape, with one addition: every new key names its permanent billing currency.
What a management key can and cannot do
| A management key can | A management key cannot |
|---|---|
| List, create, read, update and delete inference keys in its account | Run inference or list models |
Read its own metadata with GET /key | Create, list or revoke other management keys |
Read the account's credit totals with GET /credits | Change a key's billing currency or expiry after creation |
Management keys are created only in the dashboard: open Keys, choose Create key, and select Management as the Type. They have no currency or spending limit, and the secret is shown once. Disable or delete them on the same page.
Authority follows the creator
A management key acts with the current role of the person who created it:
Created by the personal account owner, or by an organization owner or administrator: it manages every inference key in the account.
Created by any other organization member: it sees and manages only the inference keys that member created. Keys it creates are attributed to that member.
If the creator is removed from the organization or their account is suspended, the management key stops authenticating. A role change applies to the next request.
Signing the creator out of the dashboard doesn't revoke their management keys; disable or delete them in Keys.
A management key can create keys that spend the account's balance. Keep it in the one service that provisions keys, never in application servers that only call models.
Endpoints
All management endpoints take the management key as a bearer token. Using an inference key on them returns 401 invalid_credentials.
| Method | Path | Purpose |
|---|---|---|
GET | /keys | List inference keys |
POST | /keys | Create an inference key |
GET | /keys/{hash} | Read one key |
PATCH | /keys/{hash} | Rename, disable or change the limit |
DELETE | /keys/{hash} | Revoke a key |
GET | /key | Inspect the management key itself |
GET | /credits | Read credit totals in one currency |
{hash} is the key's non-secret hash field: the SHA-256 hex digest of the full key. You can compute it from a leaked key when you have no other record of it.
Every key object has the fields described in API keys, without is_management_key.
List keys
curl "https://api.numes.ai/api/v1/keys?include_disabled=true&offset=0" \ -H "Authorization: Bearer $NUMES_MANAGEMENT_KEY"Query parameters
include_disabledbooleanDefaultfalseInclude disabled keys. Deleted keys are never listed.
offsetintegerDefault0Number of keys to skip.
The response is {"data": [...]} with at most 100 keys per page. Only inference keys are returned. To read everything, advance offset by the number of keys returned until a page has fewer than 100.
Create a key
curl -X POST "https://api.numes.ai/api/v1/keys" \ -H "Authorization: Bearer $NUMES_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "customer-4821", "billing_currency": "USD", "limit": 50, "limit_reset": "monthly" }'Request body
namestringRequired1–100 characters after trimming.
billing_currencystringRequiredUSDorBTC. Permanent. The currency must be available to your account.limitnumber | nullSpending limit in the key's currency, zero or greater. Omit or send
nullfor no limit. Parsed as an exact decimal.limit_resetstring | nulldaily,weeklyormonthly(UTC windows). Requireslimit. Omit or sendnullfor a lifetime limit.expires_atstring | nullA future time in UTC, such as
2026-12-31T23:59:59Z. Times with a non-UTC offset are rejected.
The response is 201 Created:
{ "data": { "hash": "3b7c9e1f5a2d4c6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e", "label": "sk-example-v1-8d2e...41b7", "name": "customer-4821", "billing_currency": "USD", "disabled": false, "limit": 50, "limit_remaining": 50, "limit_reset": "monthly", "usage": 0, "usage_daily": 0, "usage_weekly": 0, "usage_monthly": 0, "created_at": "2026-09-28T10:04:11Z", "updated_at": null, "expires_at": null }, "key": "sk-..."}key is the secret. It appears only in this response, so store it immediately. Keep data.hash with your own customer record to manage the key later without the secret.
Read a key
curl "https://api.numes.ai/api/v1/keys/3b7c9e1f5a2d4c6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e" \ -H "Authorization: Bearer $NUMES_MANAGEMENT_KEY"Returns {"data": {...}}. A key that doesn't exist, was deleted, or is outside the management key's authority returns 404 not_found.
Update a key
curl -X PATCH "https://api.numes.ai/api/v1/keys/3b7c9e1f5a2d4c6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e" \ -H "Authorization: Bearer $NUMES_MANAGEMENT_KEY" \ -H "Content-Type: application/json" \ -d '{ "limit": 75, "limit_reset": "monthly" }'Request body (at least one field)
namestringNew name, 1–100 characters.
disabledbooleantruestops the key authenticating;falsere-enables it.limitnumber | nullNew spending limit.
nullremoves the limit and its reset window.limit_resetstring | nulldaily,weekly,monthlyornull. Setting a window on a key without a limit is rejected.
billing_currency and expires_at cannot be changed; sending them, or any other unknown field, returns 400 invalid_request. The response is the updated key.
Delete a key
curl -X DELETE "https://api.numes.ai/api/v1/keys/3b7c9e1f5a2d4c6e8f0a1b3c5d7e9f1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e" \ -H "Authorization: Bearer $NUMES_MANAGEMENT_KEY"{ "deleted": true }Deletion is permanent: clients using the key get 401 immediately and a later GET /keys/{hash} returns 404. Past usage remains in Activity.
Errors
| Status | error_type | Cause |
|---|---|---|
400 | invalid_request | Unknown field, invalid value, limit_reset without limit, non-UTC expires_at, currency not available, missing currency on /credits |
401 | invalid_credentials | Missing, invalid, disabled, deleted or wrong-kind key |
404 | not_found | No key with that hash within the management key's authority |
413 | payload_too_large | Body over 32 KiB |
415 | unsupported_media_type | Body sent without Content-Type: application/json |
429 | rate_limited | Too many requests or repeated failed authentications; honor Retry-After |
Automation recipes
The recipes read the management key from NUMES_MANAGEMENT_KEY.
Provision a key per customer with a monthly limit
Create one key per customer so each has its own budget and its own breakdown in Activity.
import osimport requestsBASE_URL = "https://api.numes.ai/api/v1"HEADERS = {"Authorization": f"Bearer {os.environ['NUMES_MANAGEMENT_KEY']}"}def provision_customer_key(customer_id: str, monthly_usd: int) -> tuple[str, str]: """Return (hash, secret). Store the secret in your secret manager.""" response = requests.post( f"{BASE_URL}/keys", headers=HEADERS, json={ "name": f"customer-{customer_id}", "billing_currency": "USD", "limit": monthly_usd, "limit_reset": "monthly", }, timeout=10, ) response.raise_for_status() body = response.json() return body["data"]["hash"], body["key"]Rotate a key
Create the replacement with the same settings, deploy it, then delete the old key. The old key keeps working until the final step, so there is no gap.
def rotate_key(old_hash: str) -> tuple[str, str]: old = requests.get(f"{BASE_URL}/keys/{old_hash}", headers=HEADERS, timeout=10) old.raise_for_status() current = old.json()["data"] created = requests.post( f"{BASE_URL}/keys", headers=HEADERS, json={ "name": current["name"], "billing_currency": current["billing_currency"], "limit": current["limit"], "limit_reset": current["limit_reset"], }, timeout=10, ) created.raise_for_status() body = created.json() return body["data"]["hash"], body["key"]def retire_key(old_hash: str) -> None: """Call after every deployment has switched to the new key.""" requests.delete(f"{BASE_URL}/keys/{old_hash}", headers=HEADERS, timeout=10).raise_for_status()Between the two calls, verify the rollout by calling GET /key with the new secret from each environment.
Disable a leaked key
If a secret appears somewhere it should not, compute its hash and disable it at once. Disabling is reversible, so you can investigate before deleting.
import hashlibdef disable_leaked_key(plaintext: str) -> None: key_hash = hashlib.sha256(plaintext.encode()).hexdigest() response = requests.patch( f"{BASE_URL}/keys/{key_hash}", headers=HEADERS, json={"disabled": True}, timeout=10, ) response.raise_for_status()Then review what the key spent in Activity, issue a replacement, and delete the leaked key.
Report spend per key
List every key and read its settled spend for the current UTC month:
def monthly_spend() -> dict[str, float]: spend, offset = {}, 0 while True: response = requests.get( f"{BASE_URL}/keys", headers=HEADERS, params={"include_disabled": "true", "offset": offset}, timeout=10, ) response.raise_for_status() page = response.json()["data"] for key in page: spend[key["name"]] = key["usage_monthly"] if len(page) < 100: return spend offset += len(page)Values are in each key's own currency; never add USD and BTC keys together. For exact arithmetic, parse the raw JSON numbers as decimals (for example json.loads(text, parse_float=Decimal) in Python).
Read balances
GET /credits reports the account's credit totals in one currency. currency is required (USD or BTC); balances in different currencies are never combined.
curl "https://api.numes.ai/api/v1/credits?currency=USD" \ -H "Authorization: Bearer $NUMES_MANAGEMENT_KEY"{ "data": { "currency": "USD", "total_credits": 250, "total_usage": 61.402116 }}Response `data`
currencystringThe requested currency. BTC values are in sats.
total_creditsnumberEverything credited to the account, net of refunds, disputes and corrections.
total_usagenumberLifetime settled usage charges.
total_credits - total_usage is the posted balance. It doesn't subtract funds held for running requests; Available in Billing does. Per-request cost is in GET /generation.