Key Management
How admins mint, scope, rotate, and revoke API keys and manage tenant access.
Key management is the control plane of the gateway. It's where admins mint keys, decide what each key can do, and manage per‑tenant access to products and rate limits. This is the feature everything else depends on.
The management API
Management operations live on a separate, internal API — not the public /v1/* surface.
- It binds to loopback only (
127.0.0.1, default port8081) and is never exposed publicly, regardless of how the public API is bound. Reach it locally or over an SSH tunnel. - Every operation is a
POSTto/internal/keymgmt/<action>. - It uses its own auth: a single
x-mgmt-secretheader carrying a signed, action‑bound envelope (distinct from the key id + secret used by/v1/*), with a tighter ±60s clock skew and its own nonce replay protection.
Because it can mint credentials, the management API is operator‑only. Keep it behind loopback/SSH; never route it through a public load balancer.
Who can manage what
Authority follows the role hierarchy client < admin < superadmin, and the gateway never trusts the caller's claims — it enforces these rules itself:
- superadmin — cross‑tenant; may manage any tenant's keys and platform‑wide settings.
- admin — bound to its own tenant; manages keys for its own principal only.
- client — bound to its own tenant and its own user.
An actor can only mint a key whose role is at or below its own, and only with scopes that fit that key's role tier. A client key can never be granted admin/super scopes.
Operations
All paths below are POST /internal/keymgmt/<action>.
Keys
| Action | Body | Result |
|---|---|---|
key/create | { targetId, userId, role, principalKind?, grantedScopes[], allowedIps[], rateLimit } | Creates a key. Returns { keyId, secret, createdAt } — secret shown once. |
keys/list | { targetId, userId? } | Lists keys (never secrets). Non‑super callers see only their own principal. |
key/rotate | { keyId } | Mints a replacement and revokes the old key. Returns the new { keyId, secret }. |
key/revoke | { keyId } | Revokes a key. Idempotent; unknown/out‑of‑scope ids return { ok: true } without leaking. |
key/scopes | { keyId, grantedScopes[] } | Replaces the key's granted scopes (must fit its role tier). |
Tenant features & user access
| Action | Body | Result |
|---|---|---|
feature/set | { targetId, feature, enabled } | Turns a product on/off for a tenant (admin+). |
user/disable-api | { targetId, userId, api } | Disables one API for a user (admin+). |
user/enable-api | { targetId, userId, api } | Re‑enables it. |
Rate limits
| Action | Body | Result |
|---|---|---|
rate/set-default | { role, windowSec, max } | Platform‑wide default for a role (superadmin only). |
rate/set-override | { targetId, userId, windowSec, max } | Per‑principal override (superadmin only). |
rate/clear-override | { targetId, userId } | Clears an override (superadmin only). |
rate/effective | { targetId, userId, role? } | Reports the resolved limit and where it came from. |
Minting a key
A key/create request:
{
"targetId": "NEWDEVT",
"userId": 42,
"role": "client",
"grantedScopes": ["otp:read", "otp:purchase"],
"allowedIps": [],
"rateLimit": { "windowSec": 60, "max": 120 }
}Response — capture the secret now, it will not be shown again:
{
"keyId": "ak_test_1a2b…",
"secret": "9f83…",
"createdAt": "2026-07-17T10:00:00.000Z"
}Hand the keyId + secret to the integrating app; it signs requests with them as shown in the Quick Start.
allowedIps: [] means "any IP". To lock a key to known egress addresses, list them as exact IPs or CIDR ranges.
Good practices
- Least privilege. Grant only the scopes an integration needs; widen later with
key/scopes. - Rotate on leak. Any suspected exposure →
key/rotate, roll out, then the old key is already revoked. - One key per integration. Separate keys make revocation and auditing surgical. (Minting more keys does not raise quota — rate limits are per principal, not per key.)