Skip to content

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.

  • A key with the keys:manage scope (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.

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.

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.

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.

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.
  1. Create one parent key with sub-key management (keys:manage), capped for all customers together.
  2. Mint a sub-key per customer with scopes, caps, and a customer default tag.
  3. Report usage with group_by=key or group_by=tag:customer.
  4. Suspend with disabled: true; revoke with DELETE when the customer leaves.