Management API
Create and manage API keys from code, with spend limits, expiry and an audit log.
/v1/keysAvailability. RouterPlus is enabling this API region by region. On a gateway that does not serve it yet, every route on this page answers 404 not_found to an API key and 401 to a management key. Ask RouterPlus support when your region gets it.
Manage your organization's API keys from code: create keys with a spend limit, an expiry, an IP allowlist and labels; rotate and revoke them; read the audit log and the key security report. Set guardrails and their budgets, decide grants, and change the organization's data settings. It follows the key-management shape other AI gateways use (/api/v1/keys, limit, limit_reset, GET /api/v1/key), so such code works with the base URL changed.
Every route is under https://api.routerplus.com/v1/..., and https://api.routerplus.com/api/v1/... is the same route.
Management keys
The management API takes a management key (tm_mk_ and 48 hex characters), never an inference key. A management key never runs a model: POST /v1/chat/completions with one is a 401.
| Scope | What a key with it can do |
|---|---|
read | every GET: list and read keys, the audit log, the security report, guardrails, budget status, guardrail events, grants, the organization's settings |
keys | read, and create, change, rotate and revoke API keys, and put a guardrail on a key or take it off |
admin | everything: management keys, guardrails, grants and the organization's settings included |
Where a management key comes from:
- The console, at Security. The organization's owner can give any scope; an admin can give
readandkeys. - The API: a key with
admincan make keys withreadandkeys(POST /v1/management-keys). No management key can giveadmin, so a leaked admin key cannot copy itself past a revocation. - Support, for a new organization's first key.
The console lists every management key of the organization, the revoked ones too, with who made it and when it was last used, and owners and admins revoke any of them there. A revoked management key is refused at once.
A management key answers for the member who made it. It works while that member is the organization's owner, or an admin (for a key with read and keys). When the member is removed from the organization or becomes a viewer, the key answers 401 key_creator_inactive from its next request, the console shows it as inactive, and creator_active is false in GET /v1/management-keys. A key made with POST /v1/management-keys answers for the member behind the key that made it. If the member gets the role back, or is removed and later added again as an owner or admin, the member's keys work again without anyone approving them: revoke a departed member's management keys if they must stay off. Your API keys are not affected: they belong to the organization.
An organization can have 100 management keys; revoked ones count. At the bound a create answers 409 management_key_limit_reached.
| Route | Scope | What it does |
|---|---|---|
GET /v1/management-keys | admin | the organization's management keys, oldest first, 100 a page (limit up to 500, cursor); revoked keys are listed too unless include_disabled=false |
POST /v1/management-keys | admin | a new management key: name, scopes (read, keys; default ["read"]) and an optional expires_at. The answer has the raw key in key, once |
DELETE /v1/management-keys/<key_id> | admin | revoke a management key. A key cannot revoke itself (400 cannot_delete_self) |
A management key works only on the API of your organization's region (for example api-in.routerplus.com for an organization served in India); another region answers 403 wrong_region.
export TM_MANAGEMENT_KEY=tm_mk_...
curl -s https://api.routerplus.com/v1/keys -H "Authorization: Bearer $TM_MANAGEMENT_KEY"API keys
Create a key
POST /v1/keys (scope keys). The answer carries the raw key in key, once: it is not stored, only its hash. Store it now.
| Field | Type | Meaning |
|---|---|---|
name | string, 1 to 80 characters | required |
workspace_id | UUID | the workspace the key belongs to; default: the organization's default workspace |
principal_id | UUID | a principal of the organization the key belongs to (it sets the workspace) |
limit | number (USD) or null | the key's spend limit; null: none |
limit_reset | daily, weekly, monthly or null | when the limit starts again: 00:00 UTC, Monday 00:00 UTC, the 1st 00:00 UTC; null: never |
expires_at | ISO 8601 time with its zone (2027-01-01T00:00:00Z), or a date (00:00 UTC), in the future | the key stops working at this instant |
ip_allowlist | array of up to 100 CIDR ranges or addresses | the key works only from these addresses; empty or null: from anywhere. See IP allowlist for where it is available |
labels | object of up to 20 strings | your own attribution (for example {"team": "search"}); keys a-z 0-9 _ . -, values up to 200 characters |
finance_category | string, 1 to 64 characters | a cost center; default: the workspace's |
rpm_limit | integer, 1 to 300 | requests a minute; default 300 |
curl -s https://api.routerplus.com/v1/keys -H "Authorization: Bearer $TM_MANAGEMENT_KEY" \
-H "content-type: application/json" \
-d '{"name": "customer-4711", "limit": 25, "limit_reset": "monthly", "expires_at": "2027-01-01T00:00:00Z", "labels": {"customer": "4711"}}'{
"data": {
"key_id": "...", "name": "customer-4711", "key_prefix": "tm_vk_3f9c1a",
"disabled": false, "expired": false,
"limit": 25, "limit_reset": "monthly", "limit_remaining": 25, "usage": 0, "usage_in_period": 0,
"monthly_cap": null, "expires_at": "2027-01-01T00:00:00.000Z", "ip_allowlist": null,
"labels": {"customer": "4711"}, "finance_category": null, "rpm_limit": 300,
"created_via": "management_api", "rotated_from": null, "last_used_at": null, "...": "..."
},
"key": "tm_vk_3f9c1a..."
}In a key object, usage is what the key has spent since its first request and usage_in_period what it spent in its limit's current period; limit_remaining is limit minus usage_in_period. Amounts are USD.
How many keys. An organization can have 500 keys and create 120 API keys in any hour. A revoked or expired key still counts: its row is kept for your usage history and audit log. So do the keys the console makes for itself when you use the playground or a model search. At the first bound POST /v1/keys answers 409 key_limit_reached; at the second, 429 key_rate_limited with retry-after. A rotation makes a new key, so both bounds apply to it too: an organization at 500 cannot rotate a key until its limit is raised. Reuse keys where you can (one per customer or service, not one per session). If you need more, ask support: they can raise the limits for your organization.
List, read, change and revoke
| Route | Scope | What it does |
|---|---|---|
GET /v1/keys | read | the organization's API keys, oldest first, 100 a page (limit up to 500); next_cursor names the next page (?cursor=). workspace_id filters; include_disabled=true lists revoked keys too. An expired key is listed with "expired": true. |
GET /v1/keys/<key_id> | read | one key |
PATCH /v1/keys/<key_id> | keys | change name, disabled, limit, limit_reset, expires_at, ip_allowlist, labels, finance_category or rpm_limit; null clears a setting |
DELETE /v1/keys/<key_id> | keys | revoke the key (it is disabled, and the console can no longer copy it); revoking is for good, as in the console: a disabled key cannot be changed or enabled again (409 key_disabled). Revoking it again answers 200 and changes nothing |
A change applies from the key's next request on every gateway of the region. A request that is already running finishes, a revoked key's too. A change never pauses the organization's other requests. A gateway that is refreshing its copy of the key can serve it on the old settings for about a second more.
A change that takes access away is stricter: revoking a key, rotating it, setting an earlier expiry or an IP allowlist. Once a gateway has seen such a change, it does not serve any key of the organization from what it knew before. If it cannot read the organization's settings again just then, it answers 503 with retry-after until it can, and never serves the revoked key. If such a change is saved but our gateways could not be told at once, the answer is 503, once, and its message says that the change is saved and in place. Nothing needs to be sent again: we go on telling the gateways, and a repeat of the request changes nothing and answers 200. A rotation in that case answers 201 with "announce_pending": true, because the new key is shown only once; the old key stops as soon as the gateways are told.
A PATCH with the values the key already has answers the key and records nothing. An ip_allowlist is a set: the same ranges in another order, or with one named twice, are the values the key already has.
Rotate a key
POST /v1/keys/<key_id>/rotate (scope keys) with an optional grace_seconds (0 to 604,800; default 0) returns a new key with the same settings, and the raw key once. The old key stops at once, or after grace_seconds.
The two keys stay one rotation family: the new key's limit and monthly cap count what the old key spent in the period, and during the grace period the old key counts the new key's spend. Rotating never gives a key a fresh limit.
A key that a rotation replaced stays expired once its grace period is over: a later PATCH of its expires_at answers 409 key_rotated_out.
The calling key's own status
GET /v1/key with an API key (not a management key) answers the key's own status. It gets the same checks as a model call: expiry, IP allowlist, a verified email and the organization's region. The answer is up to 2 seconds old: a key that asks more often gets the same answer again.
{ "data": { "label": "customer-4711", "usage": 3.12, "limit": 25, "limit_remaining": 21.88, "limit_reset": "monthly",
"usage_in_period": 3.12, "expires_at": "2027-01-01T00:00:00.000Z", "is_free_tier": false,
"rate_limit": { "requests": 300, "interval": "1m" } } }What the controls do on a request
- Spend limit. Held like the monthly spend cap: before a request is sent, the gateway holds its maximum cost against the limit, with every other request in flight on the key across the region's gateways. A request that does not fit is refused, before any provider is called, with
429 insufficient_quota("key spend limit reached"),x-should-retry: false, and the reset time in the message and inx-tm-cap-reset(no header for a limit that never resets). The limit counts what RouterPlus bills, as the monthly cap does; requests served on your own provider keys (BYOK) cost nothing here and are not counted. A request counts in the period it started in: one still running at 00:00 UTC is yesterday's, and takes nothing from today's limit. - Expiry. From
expires_aton, every request is refused with401anderror_type: "key_expired". RouterPlus may disable a key that expired more than a day ago; it then answers as any unknown key does (401 auth). Either way the key is finished. - IP allowlist. A request from an address outside the list is refused with
403anderror_type: "ip_not_allowed". The address is the client's as our edge saw it (behind Cloudflare and our proxy), never a header your client sets. RouterPlus turns allowlists on region by region; where they are not on yet, the API refuses a non-emptyip_allowlistwith400 ip_allowlist_unavailable. An IPv4 range admits IPv4 callers and an IPv6 range IPv6 callers. Write an IPv4 range as IPv4 (10.0.0.0/8): an IPv4-mapped IPv6 entry (::ffff:10.0.0.0/104) is accepted only with a prefix of 96 or more. - Labels and finance category travel with the key's spend. A request cannot override a key's own labels.
Month and lifetime periods count the key's whole history. Day and week periods read a daily rollup that starts when this API was released: in that first week they count only the spend after the release, until our history fill has run.
Audit log
GET /v1/audit (scope read): every change to your organization's keys and management keys, made in the console or with a management key, and the identity, provider connection and routing policy changes, newest first. Rows cannot be changed or deleted.
| Parameter | Meaning |
|---|---|
since, until | ISO 8601 times with their zone (2026-11-01T00:00:00Z), or dates (00:00 UTC) |
source | governance, identity, connection or routing_policy |
action | for example key.create, key.update, key.rotate, management_key.create |
actor | for example mgmt:tm_mk_3f9c1a, console:[email protected], operator |
limit, cursor | 100 a page, up to 200; next_cursor names the next page. With action or actor, a page can hold fewer rows than limit, or none: go on while next_cursor is not null |
{ "data": [ { "source": "governance", "at": "2026-11-09T12:00:00.000Z", "actor": "mgmt:tm_mk_3f9c1a",
"action": "key.create", "subject": "...", "ip": "203.0.113.9", "detail": { "name": "customer-4711" } } ],
"next_cursor": null }A key.update row says what changed: detail.changes has each changed setting with its old and new value ({"limit": {"from": 5, "to": 7}}). Every changed setting is named, and a short value (a number, a time, a short name or list) is recorded as it is. A value of more than 96 bytes (a long name, many labels, a long allowlist) is recorded as its size in bytes, a short hash and how it starts, with a list's count or a map's keys ({"keys": 20, "bytes": 4901, "sha256": "125fe534580472c5", "starts": "…"}): the same value has the same hash. A key's secret is never in a row. ip is the caller's address in a region whose proxy passes it on to the gateways; until a region is set up for that, it is our own proxy's address.
The console shows the same rows at Audit log, with a CSV export.
Security report
GET /v1/security/keys (scope read): each active key and what is risky about it.
| Flag | Meaning |
|---|---|
no_limit | neither a spend limit nor a monthly cap |
no_expiry | no expires_at |
idle | not used for 30 days (a key never used: once it is 7 days old) |
expiring_soon | expires within 7 days |
no_ip_allowlist | usable from any address |
rotated | a newer key replaced it; it still works in its grace period |
The answer also has summary (how many of the organization's active keys have each flag: every key, not only this page's), each key's spend over 30 days (usage_30d_usd) and next_cursor. The console shows it at Security.
Limits on the API itself
Each gateway serves one organization's management requests one at a time, with a short line behind the one that is running, and at most 300 a minute. Past either, the answer is 429 with retry-after: wait that long and send the request again.
Writes (POST, PATCH, DELETE) have a budget for the whole region: 60 a minute and 600 an hour for one organization, whichever gateway takes them. Only a write that changes something counts. A write that is refused (400, 403, 404, 409, 429) or that changes nothing (a PATCH of the values a key already has, a repeated revoke) is not counted; such requests still count in the 300 a minute above. Past the budget the answer is 429 write_rate_limited with retry-after, and nothing was changed.
A revoke is never refused because other writes used the budget. DELETE /v1/keys/<key_id>, PATCH with "disabled": true and DELETE /v1/management-keys/<key_id> have an allowance of their own, ten times the budget (600 a minute, 6,000 an hour), so an organization can always revoke its keys.
A request body must arrive within 3 seconds of its headers, or the answer is 408 request_timeout and the connection is closed. A gateway reads at most two unfinished bodies of one organization at a time.
These bounds are per organization, so that one organization's calls do not use up another's share. The gateways are still shared: when a region is busy, any call can get 429 or 503 with retry-after.
GET /v1/key has the same bounds, counted apart: an organization's API keys can cause 300 status reads a minute on a gateway. An answer that is reused (up to 2 seconds old) is not a read.
Guardrails
The routes of this section, of Grants and of Organization settings exist only where guardrails are on (availability). Elsewhere they answer as a route the gateway does not serve.
The rules are on the Guardrails page. The routes:
| Route | Scope | What it does |
|---|---|---|
GET /v1/guardrails | read | every guardrail of the organization, with its assignments |
POST /v1/guardrails | admin | create one: name, description and the rules. 201 |
GET /v1/guardrails/<id> | read | one guardrail |
PATCH /v1/guardrails/<id> | admin | the fields you send replace the stored ones |
DELETE /v1/guardrails/<id> | admin | delete it and every assignment of it |
POST /v1/guardrails/<id>/assignments | keys on a key, else admin | {"target_kind": "org" | "workspace" | "member" | "key", "target_id": ...}. 201. It replaces another guardrail on the same target |
DELETE /v1/guardrails/<id>/assignments/<kind>/<target> | keys on a key, else admin | take it off a target |
GET /v1/guardrails/<id>/status | read | each budget: limit_usd (raised by an active grant), base_limit_usd, grant_id, spent_usd, remaining_usd, ready, period, resets_at, and suspended with suspended_message while a filter makes the budget count nothing (budgets and dedicated endpoints). Paged by assignment: 25 a page (limit up to 50), and next_cursor names the next page (?cursor=). A guardrail's shared budgets are one row (target_kind group) |
GET /v1/guardrails/events | read | requests a guardrail refused, newest first |
target_id is the organization's id, a workspace id, a member's email or a key id. An organization has at most 200 guardrails. A new key made by rotation gets the old key's guardrail, and both keys share its budgets during the grace period.
GET /v1/guardrails/events takes since, until, action, reason, key_id, guardrail_id, limit (100, up to 500) and cursor. Each event has event_id, key_id, request_id, guardrail_ids, action, reason, model, detail and created_at, and the answer has next_cursor. An event stands for detail.count equal refusals of about a minute (Guardrail events). Events never hold request text.
Grants
A grant is a time-boxed exception to a budget, a key limit or a guardrail's model rules (Grants).
| Route | Scope | What it does |
|---|---|---|
GET /v1/grants | read | ?status=, state= (pending, scheduled, active, expired, rejected, cancelled), target_type=, key_id=, guardrail_id=, limit= |
GET /v1/grants/<id> | read | one grant and its events |
POST /v1/grants | admin | request one; requested_by names a member who asked |
POST /v1/grants/<id>/approve | admin | {"note": "..."} optional |
POST /v1/grants/<id>/reject | admin | |
POST /v1/grants/<id>/cancel | admin | end a pending or approved grant |
A request:
curl https://api.routerplus.com/v1/grants \
-H "Authorization: Bearer $TM_MANAGEMENT_KEY" \
-H "Content-Type: application/json" \
-d '{"target": {"type": "budget", "guardrail_id": "'$GUARDRAIL_ID'", "budget_id": "monthly"},
"change": {"limit_usd": 800},
"expires_at": "2026-12-01T00:00:00Z",
"reason": "launch week"}'target.typeisbudget(withguardrail_id,budget_idand, optionally,target_kindandtarget_idfor one assignment),key_limit(withkey_id) orguardrail(withguardrail_id).changeis{"limit_usd": ...}, or{"allow_model": ...}forguardrail.starts_atis optional (now).expires_atis at most 90 days after it.- The grant waits,
pending, until an owner or admin decides. Whoever asked for it cannot approve it (403self_approval): not the same management key, the same API key, or the member behind them. A request that names a member withrequested_byhas two such parties: the member, and the management key that sent it. Neither approves it. - One requester may file 10 requests an hour, and an organization may have 50 pending. Past that, the answer is
429rate_limit. - A grant that is missing, or already decided, answers
404or409at once. - At most 8 approved
guardrailgrants whoseallow_modelhas a*or?apply to one guardrail. Approving one more answers409too_many_pattern_grants.
An API key manages its own grants with GET /v1/key/grants, POST /v1/key/grants, GET /v1/key/grants/<id> and POST /v1/key/grants/<id>/cancel, called with the key itself (tm_vk_). It may ask to raise its own limit, or to change a guardrail that covers it; it never approves. One API key may have 5 pending requests, and all API keys of the organization
- 40An API key may make 20 requests and cancels a minute, and all API keys of the organization together may file 20 requests a minute and 60 an hour; past that,
429rate_limit. A key made by a rotation lists, reads and cancels the requests the old key filed.
Organization settings
| Route | Scope | What it does |
|---|---|---|
GET /v1/org | read | hipaa_mode, request_log, request_log_until, retention_days, parallel_monitors (the Parallel monitors that turning HIPAA mode on would cancel) |
PATCH /v1/org | admin | change them |
hipaa_mode:trueturns HIPAA mode on at once, and setsrequest_logto"off"with it. When the organization has Parallel monitors,trueneeds"confirm": "cancel parallel monitors": HIPAA mode cancels them for good. Without it the answer is409confirmation_requiredwith their number.falseneeds"confirm": "turn off hipaa mode", else409confirmation_required;request_logstays"off".request_log: only"off", which opts the organization out of the request log.retention_days: 1 to 400, ornullfor the defaults. It shortens how long guardrail events are kept. A value above 400 is refused: it would be longer than the default.
A change applies to the next request on every gateway.
Errors
The management API answers its own errors as {"error": {"code": <HTTP status>, "message": "...", "type": "..."}}:
| Status | type | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_expiry, cannot_delete_self | a field, a time, a query parameter or a cursor is wrong; the message names it. invalid_expiry: an expires_at that is not in the future |
| 400 | invalid_guardrail, unsupported, grant_not_raise, grant_too_long | a field of a guardrail or a grant is wrong; the message names it |
| 400 | ip_allowlist_unavailable | IP allowlists are not on in this region yet |
| 401 | unauthorized, key_expired | no management key, an unknown or revoked one, or an expired one |
| 401 | key_creator_inactive | the member who made this management key is no longer the owner or an admin of the organization |
| 403 | insufficient_scope, scope_not_delegable, wrong_region | the key lacks the route's scope; a management key tried to give admin; another region's organization |
| 403 | forbidden, self_approval | an API key asked about another key's grant; the requester tried to approve its own grant |
| 404 | not_found | no such route, or no such key in your organization |
| 405 | method_not_allowed | the path exists with another method |
| 408 | request_timeout | the request body did not arrive in time; the connection is closed |
| 409 | conflict, key_disabled, workspace_disabled, principal_disabled | for example changing or rotating a revoked key, or a new key in a workspace whose default principal is disabled (name an enabled principal_id) |
| 409 | key_limit_reached | the organization has as many keys as it may; revoked and expired keys count, and the console's own. A rotation is refused too |
| 409 | management_key_limit_reached | the organization has as many management keys as it may; revoked ones count |
| 409 | key_rotated_out | expires_at of a key that a rotation replaced, after its grace period |
| 409 | hard_limit, grant_not_pending, grant_not_open, grant_expired, too_many_pattern_grants, confirmation_required, spend_history_building, budget_filter_unavailable | a grant on a hard limit, a grant that is already decided or has ended, a ninth pattern grant on one guardrail, HIPAA mode off without the confirmation, a budget whose spend history is not whole yet (when a budget can be set), or a budget filter that an organization with a dedicated endpoint cannot use (budgets and dedicated endpoints) |
| 413 | payload_too_large | a body over 256 KB |
| 429 | key_rate_limited | the organization created as many keys this hour as it may; retry-after says when the next one fits |
| 429 | write_rate_limited | the organization made as many management writes that changed something this minute or this hour as it may (revokes have an allowance of their own); retry-after says when the next one fits |
| 429 | rate_limit | too many grant requests (see Grants) |
| 429 | rate_limited | the organization sent more management requests, or its keys read their status more often, than one gateway takes in a minute, or too many of its request bodies are being read; wait retry-after |
| 503 | unavailable | the API could not read or write its data just now, or two changes to the organization met: nothing was changed; wait retry-after and send it again. Or a change that takes access away is saved and the region's gateways could not be told at once: the message says so, nothing needs to be sent again, and a repeat answers 200 |
| 429, 503 | (as on every route) | the region is busy: wait retry-after |
These refusals are the management API's own. An API key's own errors on a model call (key_expired, ip_not_allowed, insufficient_quota) are in Errors.
For agents: /docs/api-management.md and /llms.txt