One sub-key per customer
If your product resells AI features to its own customers, give each customer a sub-key: a key minted by your server’s key, with its own spend caps and attribution tags. You get per-customer limits and per-customer usage without creating anything in the dashboard for each one.
How sub-keys work
Section titled “How sub-keys work”- A key with the
keys:managescope (the parent) creates, lists, edits, disables, and revokes its own sub-keys through/v1/keys. - There is one level only: a sub-key never gets
keys:manage, so it cannot mint keys of its own. - A sub-key never exceeds its parent: its scopes, models, caps, rates, output ceiling, and expiry must all fit inside the parent’s, and it requires at least the parent’s required tags. Anything wider is refused with
400 child_exceeds_parent. - Every call on a sub-key reserves its cost on the sub-key and on the parent, so the parent’s caps bound the whole family.
- A sub-key inherits the parent’s environment and mode: a
bel_test_…parent mints sandbox sub-keys. A zero-retention parent only mints zero-retention sub-keys. - If the parent is disabled, revoked, or expired, all its sub-keys stop working.
Set up the parent key
Section titled “Set up the parent key”In your organization’s dashboard, create a key for your server with sub-key management turned on (the keys:manage scope) and the caps you want for all your customers combined. Keys created in the dashboard also carry inference, embeddings, and usage:read. Keep it on your server only — it is the key that controls the others. Store it in BELAREL_API_KEY.
Create a sub-key for a customer
Section titled “Create a sub-key for a customer”POST /v1/keys returns the new key with its token. The token is shown once: store it, or hand it to your customer, right away.
const BASE = 'https://api.belarel.com/v1';const headers = { Authorization: `Bearer ${process.env.BELAREL_API_KEY}`, 'Content-Type': 'application/json',};
async function belarel<T>(path: string, init: RequestInit = {}): Promise<T> { const res = await fetch(`${BASE}${path}`, { ...init, headers: { ...headers, ...init.headers } }); const body = await res.json(); if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`); return body as T;}
interface ApiKey { id: string; name: string; prefix: string; status: string; caps: { usd_day: number | null; usd_month: number | null; rpm: number; tpm: number | null; max_in_flight: number; max_output_tokens: number }; default_tags: Record<string, string>; disabled_at: string | null;}
export async function createCustomerKey(customerId: string) { const key = await belarel<ApiKey & { token: string }>('/keys', { method: 'POST', body: JSON.stringify({ name: `customer ${customerId}`, scopes: ['inference'], default_tags: { customer: customerId }, caps: { usd_day: 5, usd_month: 50, rpm: 60, max_in_flight: 4 }, }), }); // key.token is returned only now — save it. return { id: key.id, token: key.token };}What the body accepts:
| Field | Meaning |
|---|---|
name |
1–80 characters. Shown as the label in usage by key. |
scopes |
Any of inference, embeddings, usage:read. Omitted: the parent’s scopes without keys:manage. |
allowed_model_keys |
Model ids this key may call. Each must be in your organization’s API catalog (400 model_not_in_catalog). Omitted: the parent’s list. |
caps |
usd_day, usd_month, rpm, tpm, max_in_flight, max_output_tokens. Each omitted cap is inherited from the parent. |
default_tags |
Tags added to every call made with this key. The parent’s default tags are merged in; the sub-key’s values win. |
required_tags |
Tags every model call must carry (400 missing_tags otherwise), added to the parent’s. Reads such as /v1/usage never need them. |
expires_at |
ISO 8601 with an offset. Omitted: the parent’s expiry. |
data_mode |
standard or zero_retention. |
Unknown fields are refused with 400 invalid_request.
Track usage per customer
Section titled “Track usage per customer”GET /v1/usage on the parent key covers the parent and all its sub-keys. Group by key to get one row per key, labeled with its name — or by tag:customer if you tag calls with default_tags as above.
interface UsageRow { group: string | null; // key id with group_by=key label: string | null; // key name with group_by=key requests: number; failed_requests: number; input_tokens: number; output_tokens: number; credits: number; cost_usd: number;}
export async function usageByCustomer(from: string, to: string) { const qs = new URLSearchParams({ from, to, group_by: 'key' }); const { data } = await belarel<{ data: UsageRow[] }>(`/usage?${qs}`); return data.map((r) => ({ key: r.group, name: r.label, requests: r.requests, costUsd: r.cost_usd }));}from and to take ISO dates or Unix timestamps; the window defaults to the last 30 days and holds at most 93. Costs are at your organization’s price. The parent’s own calls appear as a row too.
A customer holding a sub-key can read its own caps, today’s and this month’s spend, and what remains with GET /v1/key — and nothing about your other customers.
From the parent, GET /v1/keys lists every sub-key — revoked ones included, so filter on status if you only want live keys. Each entry carries spent_usd_day, the sub-key’s spend counted today (UTC), in USD.
Change limits, disable, or revoke
Section titled “Change limits, disable, or revoke”PATCH /v1/keys/{id} updates name, allowed_model_keys, required_tags, default_tags, caps, and expires_at, and switches the key off and on with disabled. Send only what changes. null clears a dollar cap, tpm, the model list, or the expiry — unless the parent has that limit, in which case the change is refused with child_exceeds_parent.
// Raise a customer's monthly cap.await belarel<ApiKey>(`/keys/${keyId}`, { method: 'PATCH', body: JSON.stringify({ caps: { usd_month: 100 } }) });
// Suspend a customer (reversible).await belarel<ApiKey>(`/keys/${keyId}`, { method: 'PATCH', body: JSON.stringify({ disabled: true }) });
// Restore them.await belarel<ApiKey>(`/keys/${keyId}`, { method: 'PATCH', body: JSON.stringify({ disabled: false }) });
// Revoke for good.await belarel<{ id: string; deleted: true }>(`/keys/${keyId}`, { method: 'DELETE' });- A disabled or revoked sub-key answers
401 invalid_api_key, the same answer as an unknown key. - DELETE is definitive: a revoked key can’t be re-enabled (
400 key_revoked). Calls already in flight finish and are billed normally. - An id that isn’t one of the caller’s sub-keys answers
404 not_found.
- Create one parent key with sub-key management (
keys:manage), capped for all customers together. - Mint a sub-key per customer with
scopes,caps, and acustomerdefault tag. - Report usage with
group_by=keyorgroup_by=tag:customer. - Suspend with
disabled: true; revoke withDELETEwhen the customer leaves.

