Management API
Manage keys, guardrails, workspaces and more with a management key.
Use the management API to control your organization's keys, guardrails, grants, presets, workspaces, notifications and exports. It also reports usage, spend forecasts, savings, security and audit data.
Every route is served under /v1/... and under /api/v1/.... The two paths act the same.
curl https://dev2-api.routerplus.com/v1/keys \
-H "Authorization: Bearer $TM_MANAGEMENT_KEY"Authentication
Send a management key as a Bearer token:
Authorization: Bearer tm_mk_...- A management key belongs to one organization. Every route acts on that organization only. An id from another organization returns
404. - A management key never authenticates an inference request. An API key (
tm_vk_...) never authenticates a management route, with two exceptions:GET /v1/keyreturns the calling API key's own status.- An API key can request grants for itself and read or cancel them. See Grants.
- A missing, disabled or expired management key returns
401.
Requests and responses
- Send JSON with
content-type: application/json. A request body is at most 256 KiB. - One object comes back as
{"data": {...}}. A list comes back as{"data": [...]}. - Money is in USD. Times are ISO 8601 in UTC. Ids are UUIDs.
- The audit log records every write, with the management key's prefix as the actor.
Errors
{"error": {"code": 400, "message": "limit: must be a number of USD from 0 to 10,000,000", "type": "invalid_request"}}code is the HTTP status. type is a stable string you can match on.
| Status | Meaning |
|---|---|
| 400 | The input is not valid. message names the field. |
| 401 | No valid key. |
| 403 | The key is valid but cannot do this. For example, an API key cannot approve a grant. |
| 404 | No such record in your organization, or no such route. |
| 405 | The route does not take this method. |
| 409 | A conflict: a record exists, a version is stale, or the record is in the wrong state. |
| 413 | The body is larger than 256 KiB. |
Key status (API key)
| Route | Does |
|---|---|
GET /v1/key | Returns the calling API key's own status. Authenticate with Authorization: Bearer tm_vk_.... |
{"data": {"label": "ci", "usage": 1.25, "limit": 10, "limit_remaining": 8.75, "limit_reset": "monthly",
"expires_at": null, "is_free_tier": false, "rate_limit": {"requests": 300, "interval": "1m"}, "include_byok_in_limit": false}}usageis the spend in the current limit period. With nolimit_reset, it is the spend over the key's life.- While an active grant raises the key's limit,
limitis the granted value. The answer then also hasbase_limit(the key's own limit) andgrant_id. - An expired key returns
401 key_expired.
API keys
| Route | Does |
|---|---|
GET /v1/keys | Lists active keys. Query: workspace_id, include_disabled=true. |
POST /v1/keys | Creates a key. Returns 201 with key (the raw key) and data. |
GET /v1/keys/:id | Reads one key. |
PATCH /v1/keys/:id | Changes the fields you send. |
DELETE /v1/keys/:id | Disables the key. It stops working at once. |
POST /v1/keys/:id/rotate | Creates a new key with the same settings. |
The create and rotate responses show the raw key once. Store it then. You cannot read it again.
Create fields
| Field | Type | Notes |
|---|---|---|
name | string | Required. 1 to 80 characters. |
workspace_id | UUID | Default: the default workspace. |
principal_id | UUID | Default: the workspace's service principal. |
limit | number or null | USD per limit_reset period. |
limit_reset | string or null | daily, weekly, monthly, or null for no reset. |
include_byok_in_limit | boolean | Count BYOK inference cost in the limit. |
expires_at | ISO time or null | Must be in the future. The key stops working then. |
ip_allowlist | array or null | CIDR ranges or addresses. Null allows any address. |
labels | object | At most 20. Keys: a-z 0-9 _ . -, 1 to 40 characters. Values: at most 200 characters. |
finance_category | string or null | 1 to 64 characters. |
Update
PATCH takes name, disabled, and every create field except workspace_id and principal_id. Send null to clear limit, expires_at, ip_allowlist or finance_category.
Rotate
Rotate takes {"grace_seconds": 0..604800} (default 0).
- With 0, the old key is disabled at once. Otherwise the old key expires after the grace period.
- The new key has
rotated_fromset to the old key's id. - A disabled or expired key cannot be rotated (
409).
Key object
{"key_id": "...", "name": "ci", "key_prefix": "tm_vk_3f9a1c", "workspace_id": "...", "principal_id": "...",
"disabled": false, "limit": 10, "base_limit": 10, "grant_id": null, "limit_reset": "monthly", "limit_remaining": 8.75,
"usage": 1.25, "include_byok_in_limit": false, "expires_at": null, "ip_allowlist": ["10.0.0.0/8"], "labels": {"team": "core"},
"finance_category": null, "rpm_limit": 300, "created_at": "...", "created_via": "management_api",
"rotated_from": null, "last_used_at": "..."}While an active grant raises the key's limit, limit and limit_remaining use the granted value. base_limit is the key's own limit and grant_id names the grant. With no grant, base_limit equals limit and grant_id is null.
Management keys
| Route | Does |
|---|---|
GET /v1/management-keys | Lists management keys. |
POST /v1/management-keys | {name, expires_at?}. Returns 201 with key (shown once). |
DELETE /v1/management-keys/:id | Disables the key. A key cannot delete itself (400 cannot_delete_self). |
Workload identities
A workload identity lets a CI job or cloud workload trade its OIDC token for a short-lived API key.
| Route | Does |
|---|---|
GET /v1/workload-identities | Lists identities. |
POST /v1/workload-identities | Creates one. Returns 201. |
DELETE /v1/workload-identities/:id | Deletes one. |
| Field | Notes |
|---|---|
name | Required. 1 to 100 characters. |
workspace_id | Required. |
principal_id | Default: the workspace's service principal. |
issuer | An https URL, such as https://token.actions.githubusercontent.com. |
audience | The token's aud. |
subject_pattern | The token's sub. * matches any run of characters. |
required_claims | Claim names and the exact values they must have. At most 20. |
key_ttl_seconds | 300 to 86,400. Default 3,600. |
Guardrails
A guardrail is a set of rules. It applies only when you assign it to the org, a workspace, a member or a key. When several guardrails apply to a request, the strictest rule wins.
| Route | Does |
|---|---|
GET /v1/guardrails | Lists guardrails with their assignments. |
POST /v1/guardrails | {name, description?, ...rules}. Returns 201. |
GET /v1/guardrails/:id | Reads one guardrail. |
PATCH /v1/guardrails/:id | Replaces the rules you send. version goes up by 1. |
DELETE /v1/guardrails/:id | Deletes the guardrail and its assignments. |
POST /v1/guardrails/:id/assignments | {target_kind, target_id}. Returns 201. |
DELETE /v1/guardrails/:id/assignments/:kind/:target | Removes one assignment. URL-encode an email. |
GET /v1/guardrails/:id/status | Spend of each budget in its current period. |
GET /v1/guardrails/events | Requests a guardrail blocked, redacted or flagged. |
Rules
| Field | Type |
|---|---|
allowed_models, blocked_models | Model patterns. * matches any run of characters. |
allowed_providers, blocked_providers | Provider names. |
zdr | boolean. Only providers that keep no data. |
data_collection | "deny": only providers that do not train on data. |
allowed_regions | Region codes such as us, eu, in. |
blocked_infrastructure | Clouds such as aws, gcp. |
budgets | Up to 20 of {id?, limit_usd, period, model?, provider?, include_byok?, shared?}. period: day, week, month, lifetime. shared: true makes one budget for every target of the guardrail. |
limit_usd, reset_interval, include_byok_in_budgets | One budget, in the short form. |
rpm, tpm, max_output_tokens | Integers. |
server_tools | allow or block. |
content_rules | Up to 50 of {id?, kind, preset?, pattern?, flags?, action, label?}. kind: pii, regex, injection. action: redact, block, flag. |
on_scan_limit | allow or block, for text too long to scan in full. |
overridable | boolean. Default true. false makes the guardrail a hard limit. See Hard limits. |
An unknown field returns 400 invalid_guardrail. A regex that can run too long returns 400 invalid_regex_pattern.
Assignments
target_kind | target_id |
|---|---|
org | Your org id, or leave it out. |
workspace | A workspace id. |
member | A member's email. |
key | An API key id. |
A target has at most one guardrail. A new assignment on a target replaces the old one. The response names the replaced guardrail in replaced_guardrail_id.
Status
A status entry:
{"budget_id": "b0", "budget_key": "...", "target_kind": "key", "target_id": "...", "shared": false, "model": null, "provider": null,
"include_byok": false, "limit_usd": 5, "base_limit_usd": 5, "grant_id": null, "spent_usd": 1.25, "remaining_usd": 3.75,
"period": "day", "period_start": "2026-10-31T00:00:00.000Z", "resets_at": "2026-11-01T00:00:00.000Z"}limit_usd is the limit RouterPlus enforces now. base_limit_usd is the guardrail's own limit. While an active grant raises the budget, grant_id names it and limit_usd is the granted value.
Events
| Query | Notes |
|---|---|
since, until | Time range. |
action | blocked, redacted or flagged. |
key_id | One key. |
limit | At most 500. Default 100. |
Events come newest first. They never hold the matched text.
Grants
A grant is a time-boxed exception to a guardrail budget, an API key's limit, or a guardrail's model rules. A member or an API key requests a grant. An admin approves or rejects it. An approved grant applies from starts_at until expires_at. After that, the base value applies again.
In the console, open Access > Grants.
curl https://dev2-api.routerplus.com/v1/grants \
-H "Authorization: Bearer $TM_MANAGEMENT_KEY" \
-H "content-type: application/json" \
-d '{"target": {"type": "key_limit", "key_id": "8c0d..."},
"change": {"limit_usd": 50},
"expires_at": "2026-11-09T00:00:00Z",
"reason": "Load test for the launch"}'| Route | Does | Who may call |
|---|---|---|
GET /v1/grants | Lists grants, newest first. | Management key. An API key sees only the grants it requested. |
POST /v1/grants | Requests a grant. Returns 201 with the grant in pending status. | Management key, or an API key for itself. |
GET /v1/grants/:id | Reads one grant with its events. | Management key, or the API key that requested it. |
POST /v1/grants/:id/approve | {note?}. Approves a pending grant. | Management key only. An API key gets 403. |
POST /v1/grants/:id/reject | {note?}. Rejects a pending grant. | Management key only. An API key gets 403. |
POST /v1/grants/:id/cancel | {note?}. Ends a pending or approved grant before expires_at. | Management key, or the API key that requested it. |
note is optional, at most 500 characters. An approve or cancel applies to the next request on every gateway.
Request fields
| Field | Notes |
|---|---|
target | Required. What the grant changes. See the table below. |
change | Required. {"limit_usd": n} for budget and key_limit grants. {"allow_model": "<id or pattern>"} for guardrail grants. |
starts_at | ISO time. Default: now. It cannot be in the past (5 minutes of clock skew is allowed). |
expires_at | Required. ISO time after now and after starts_at. At most 90 days after starts_at. |
reason | Required. 1 to 500 characters. |
requested_by | Management key only. The email of an active member to record as the requester. |
target:
type | Fields | Changes |
|---|---|---|
budget | guardrail_id, budget_id, and optionally target_kind and target_id | Raises one budget of a guardrail to change.limit_usd. With target_kind and target_id, the grant raises the budget for that assignment only. Without them, it raises it for every assignment. A shared budget takes no target. |
key_limit | key_id | Raises an API key's limit to change.limit_usd. The key must have a limit. |
guardrail | guardrail_id, and optionally target_kind and target_id | Lets change.allow_model pass the guardrail's allowed and blocked model lists. With a target, it covers only the keys under that target. Every other guardrail still applies. |
target_kind is org, workspace, member or key, as for assignments.
Rules for limits:
change.limit_usdis a number of USD from 0 to 10,000,000. It must be more than the current limit.- When several active grants raise the same limit, the highest one applies.
- A granted limit never goes below the base limit and never above the org's hard limit for that period.
What an API key may request:
- A
key_limitgrant for its own key only. - A
budgetgrant only on a guardrail that applies to the key. Without a target, the grant raises the budget of the most specific assignment that covers the key. - A
guardrailgrant only on a guardrail that applies to the key. The grant covers that key only. - An API key cannot set
requested_by.
Grant object
{"data": {"id": "...", "status": "approved", "state": "active",
"target": {"type": "budget", "guardrail_id": "...", "budget_id": "b0", "target_kind": "workspace", "target_id": "..."},
"change": {"limit_usd": 50}, "base_limit_usd": 20, "workspace_id": "...",
"starts_at": "...", "expires_at": "...", "reason": "Load test for the launch",
"requested_by": "key:...", "approved_by": "tm_mk_...", "decided_by": "tm_mk_...", "decided_at": "...",
"decision_note": null, "cancelled_by": null, "cancelled_at": null, "created_at": "...", "updated_at": "..."}}statusispending,approved,rejectedorcancelled.stateadds the time window:pending,scheduled(approved, not started),active,expired,rejectedorcancelled.base_limit_usdis the limit when the grant was requested. It is null forguardrailgrants.GET /v1/grants/:idaddsevents: each state change withaction,actor,detailandcreated_at.
List query
| Query | Notes |
|---|---|
status | pending, approved, rejected or cancelled. |
state | pending, scheduled, active, expired, rejected or cancelled. |
target_type | budget, key_limit or guardrail. |
key_id | Grants on that key's limit, or guardrail grants scoped to that key. |
guardrail_id | Grants on one guardrail. |
limit | At most 500. Default 100. |
Where grants show
- A response to a request under active grants has the header
x-tm-grants, a comma-separated list of grant ids. A refusal under active grants has it too. - Guardrail status shows
limit_usd,base_limit_usdandgrant_idper budget. - Key reads and
GET /v1/keyshow the grantedlimit,base_limitandgrant_id. - Budget alerts use the granted limits.
Notices
RouterPlus sends a notice to the org's enabled notification channels and by email when:
- a grant is requested (to org owners and admins, and admins of the grant's workspace),
- a grant is approved, rejected or cancelled (to the requester),
- an approved grant ends in less than a day (to the requester and the approver).
Hard limits
A guardrail with "overridable": false is a hard limit.
- It takes no grant. A request for a grant on it returns
409 hard_limit. - When it is assigned to the org, each of its budgets with no
modelorprovidercaps every grant that raises a budget or key limit with the same period. A raise above the cap returns409 hard_limit. - Approval checks the target again. A grant whose guardrail became a hard limit, or whose raise is now above the cap, is refused with
409 hard_limit.
Grant errors
| Status and type | When |
|---|---|
400 invalid_expiry | expires_at is not after now and after starts_at. |
400 grant_too_long | The grant lasts more than 90 days. |
400 grant_not_raise | change.limit_usd is not more than the current limit. |
403 forbidden | An API key tried to approve or reject, set requested_by, or request a grant outside its own key. |
404 | The guardrail, budget, assignment, key, member or grant is not in your org. |
409 hard_limit | The guardrail is a hard limit, or the raise is above the org's hard limit. |
409 no_limit | The key has no limit to raise. |
409 grant_not_pending | Approve or reject on a grant that is not pending. |
409 grant_not_open | Cancel on a grant that is rejected or cancelled. |
409 grant_expired | The grant's expires_at has passed. |
Presets
A preset is a versioned request template. Call it with "model": "@preset/<slug>" or "@preset/<slug>@<version>".
| Route | Does |
|---|---|
GET /v1/presets | Lists the latest version of each preset. Query: workspace_id. |
POST /v1/presets | {slug, description?, workspace_id?, template}. Creates version 1. |
GET /v1/presets/:slug | The latest version. Query version=N for an older one. |
PUT /v1/presets/:slug | {template, description?, expected_version}. Creates the next version. |
DELETE /v1/presets/:slug | Deletes every version. |
- A slug is 1 to 80 of
a-z 0-9 _ -, starting with a letter or digit. POSTreturns409 preset_existswhen the slug is in use.PUTreturns409 version_conflictwhenexpected_versionis not the latest version.
Template
| Field | Type |
|---|---|
model or models | Send one, not both. models lists 1 to 5 models, tried in order. |
provider | A provider routing object, as in a request. |
defaults | Parameters used when the request does not set them. |
overrides | Parameters that replace the request's values. |
system_prompt | Up to 20,000 characters. Put before the request's own system text. |
guardrail_id | A guardrail in your org. |
Parameters in defaults and overrides:
| Parameter | Range |
|---|---|
temperature | 0 to 2 |
top_p | 0 to 1 |
top_k | 1 to 32768 |
max_tokens, max_completion_tokens | 1 to 1,000,000 |
frequency_penalty, presence_penalty | -2 to 2 |
seed | integer |
stop | a string or up to 4 strings |
reasoning_effort | minimal, low, medium, high, xhigh |
response_format | object |
parallel_tool_calls, stream | boolean |
How a preset combines with a request
- 01
overridesbeat the request. The request beatsdefaults. - 02The template's
modelormodelsreplace the request's. - 03A request's
providerobject can only narrow the template's.onlykeeps the providers in both.ignoreadds.allow_fallbacks: false,zdr: trueanddata_collection: "deny"always win. The request'ssortandorderapply only when the template sets none. - 04On
/v1/messages, a preset setstemperature,top_p,top_k,max_tokens,stop(asstop_sequences) andstream. The other parameters apply on the OpenAI surface only.
Workspaces
| Route | Does |
|---|---|
GET /v1/workspaces | Lists workspaces. |
POST /v1/workspaces | {name, finance_category?, default_sort?}. Returns 201. |
PATCH /v1/workspaces/:id | name, disabled, finance_category, default_sort, route_policy_id. |
POST /v1/workspaces/:id/clone | {name, include}. Returns 201. |
GET /v1/workspaces/:id/members | Lists workspace members. |
PUT /v1/workspaces/:id/members | {email, role}. Adds a member or changes the role. |
GET /v1/workspaces/:id/members/:email | Reads one member. |
PUT /v1/workspaces/:id/members/:email | {role}. |
DELETE /v1/workspaces/:id/members/:email | Removes a member. |
default_sort:routing_cost,price,throughputorlatency.role:admin,memberorviewer.- The email must belong to a member of the organization.
Clone
A clone gets the source's route policy, finance category and default sort. include can list:
guardrails: the source workspace's guardrail assignment.presets: the presets scoped to the source. Each copy gets the slug<slug>-copy.members: the workspace members.
Organization settings
| Route | Does |
|---|---|
GET /v1/org | Returns org_id, name, default_sort, request_log, request_log_until, retention_days and hipaa_mode. |
PATCH /v1/org | Changes the fields below. |
| Field | Notes |
|---|---|
default_sort | As for workspaces, or null. |
request_log | on or off. With on, RouterPlus logs full requests for request_log_days (1 to 365, default 30). In HIPAA mode, on returns 409 hipaa_mode. |
retention_days | 1 to 3650, or null for the defaults. See Retention. |
hipaa_mode | true turns HIPAA mode on at once. false turns it off only with "confirm": "turn off hipaa mode". Without it the answer is 409 confirmation_required. See HIPAA mode. |
Every change writes an audit row (GET /v1/audit, action org.update). Audit rows cannot be changed or deleted.
Retention
RouterPlus deletes stored records past their limit. A job runs every 30 minutes and deletes at most 5,000 rows per table per run.
| Records | Default limit |
|---|---|
| Request labels and sessions | 400 days |
| Guardrail events | 400 days |
| MCP tool call records | 400 days |
| Eval answers and judge results | 90 days after the eval finished |
| Batch files | At their expires_at |
| Slack agent thread memory | Emptied 7 days after the last message, deleted 7 days later |
| OAuth codes and SSO sign-in states | 1 day after they expire |
| Sign-in log | 400 days |
retention_days can only make your org's limits shorter. RouterPlus uses the smaller of the default and retention_days.
- For eval results, it counts from when the eval finished.
- For batch files, it counts from their creation.
- For Slack threads, it counts from their last message.
- The sign-in log, OAuth codes and SSO states keep the defaults.
HIPAA mode
With hipaa_mode on:
- Every request gets an implicit org guardrail:
zdr: true,data_collection: "deny",server_tools: "block". Its id inx-tm-guardrailsis00000000-0000-4000-8000-000000000074. RouterPlus refuses a model with no route that meets it. - RouterPlus does not log requests. The org's
request_logsetting and any batch setting do not apply. - The response cache and the semantic cache are off. A request that asks for the cache gets an answer with
x-tm-cache: skip. - A broadcast destination with
privacy.include_content: truereturns409 hipaa_mode. RouterPlus does not send to a content destination made before HIPAA mode. - Slack agents keep no thread memory. Each answer uses only the current message. Turning HIPAA mode on empties the stored memory.
HIPAA mode is a set of data rules. It is not a business associate agreement.
Notification channels
Channels receive budget alerts when spend crosses a threshold, and grant notices.
| Route | Does |
|---|---|
GET /v1/notifications/channels | Lists channels. |
POST /v1/notifications/channels | {kind, config, enabled?, thresholds?}. Returns 201. |
PATCH /v1/notifications/channels/:id | Changes the fields you send. kind cannot change. |
DELETE /v1/notifications/channels/:id | Deletes the channel. |
POST /v1/notifications/channels/:id/test | Sends a sample alert. Returns {ok, status?, error?}. |
kind | config |
|---|---|
email | {to: [1 to 10 addresses]} |
slack | {webhook_url: "https://hooks.slack.com/..."} |
webhook | {url: "https://...", secret?}. With a secret, RouterPlus signs each request in the x-routerplus-signature header. |
thresholdsare percents of a budget, integers from 1 to 100. Default:[50, 80, 100].- Each threshold is sent once per budget period. A channel gets one alert per budget, for the highest threshold crossed.
- Alerts cover guardrail budgets and API key limits.
- The Slack URL and the webhook secret come back as
"__secret__". Send"__secret__"to keep the stored value.
Broadcast destinations
Destinations receive a trace of each request.
| Route | Does |
|---|---|
GET /v1/broadcast/destinations | Lists destinations. |
POST /v1/broadcast/destinations | {name?, kind, config, enabled?, sampling?, privacy?, filter?}. Returns 201. |
PATCH /v1/broadcast/destinations/:id | Changes the fields you send. kind cannot change. |
DELETE /v1/broadcast/destinations/:id | Deletes the destination. |
POST /v1/broadcast/destinations/:id/test | Sends one sample trace. Returns {ok, status}. |
kind | config |
|---|---|
webhook | {url, secret?, headers?} |
otlp | {endpoint, headers?, service_name?} |
langfuse | {public_key, secret_key, host?} |
datadog | {api_key, site?, service?} |
clickhouse | {url, table, database?, user?, password?} |
s3 | {bucket, region, access_key_id, secret_access_key, prefix?, endpoint?, session_token?, gzip?}. Also R2 and MinIO. |
azure_blob | {account, container, sas_token or account_key, prefix?, gzip?} |
bigquery | {project_id, dataset, table, service_account_json} |
snowflake | {account, user, private_key_pem, database, schema, table, warehouse?, role?} |
Broadcast to storage and warehouses gives the object layout and the table DDL for the storage and warehouse kinds.
| Field | Notes |
|---|---|
sampling | 0 to 1. Default 1. |
privacy | {include_content, omit_fields, hash_fields, redact_paths}. Content is left out by default. In HIPAA mode, include_content: true is refused. |
filter | {workspace_ids, key_ids, models, errors_only}. |
Secrets and header values never come back. They show as "__secret__". Send "__secret__" to keep the stored value.
BYOK connections
| Route | Does |
|---|---|
PATCH /v1/connections/:id | {"negotiated_discount_percent": 0..100} sets the discount your provider gives you on a BYOK connection. Send null to clear it. |
Cost reports use the discount (see GET /v1/activity/byok).
Activity
| Route | Does |
|---|---|
GET /v1/activity | Usage per group. |
GET /v1/activity/labels?label=<key> | Requests grouped by the value of one request label. |
GET /v1/activity/sessions | Requests grouped by session id. |
GET /v1/activity/cache | Prompt caching per model and provider. |
GET /v1/activity/cache/advice | What in your traffic costs cache reuse, with the numbers and the fix. |
GET /v1/activity/byok | Requests on your own provider connections, per connection and model. |
GET /v1/activity/forecast | Projected spend to the end of the month or quarter. See Spend forecast. |
POST /v1/activity/forecast | The same forecast, with the parameters in the body. |
GET /v1/activity/savings | What your traffic would cost on cheaper routes or models. See Savings. |
GET /v1/activity takes:
| Query | Notes |
|---|---|
from, to | UTC dates, both included. Default: the last 30 days. At most 366 days. |
group_by | day (default), model, provider, key, workspace, member, finance_category. |
key_id, workspace_id, model, provider | Filters. |
The label, session, cache and BYOK reports take from and to too.
Response rows:
| Report | Each row has |
|---|---|
/v1/activity | The group field (date, model, provider, key_id, workspace_id, member or finance_category), requests, usage_usd, byok_usage_usd, input_tokens, cached_input_tokens, cache_write_tokens, output_tokens, reasoning_tokens, cache_hit_rate. |
/v1/activity/labels | value, requests, attempts, usage_usd and the token counts. |
/v1/activity/sessions | session_id, requests, usage_usd, models, first_seen, last_seen. Query limit: at most 500. |
/v1/activity/cache | input_tokens, cached_input_tokens, cache_write_tokens, cache_hit_rate, estimated_savings_usd. |
/v1/activity/byok | requests, list_cost_usd, negotiated_discount_percent, negotiated_cost_usd (the list cost less the discount set with PATCH /v1/connections/<id>). |
cache_hit_rateis cached input tokens divided by input tokens.estimated_savings_usdis the cached tokens times the difference between the input price and the cached input price.
/v1/activity/cache/advice returns these findings:
| Finding | When |
|---|---|
low_hit_rate | A model with 1M+ input tokens and a hit rate under 20%, with the most it could save. |
cache_writes_unread | Cache writes that no later request read. |
no_session_ids | Over 30% of requests carry no session id. |
Spend forecast
GET /v1/activity/forecast projects your org's spend on RouterPlus credits to the end of the current UTC month, per group. It also gives the first day each budget or key limit is projected to run out. BYOK spend is not projected.
curl "https://demo-api.routerplus.com/v1/activity/forecast?group_by=workspace&horizon=quarter" \
-H "Authorization: Bearer $TM_MANAGEMENT_KEY"| Parameter | Notes |
|---|---|
group_by | model (default), workspace, key or label. |
label | Required with group_by=label. A label key: 1 to 40 of a-z 0-9 _ . -. |
horizon | month (default): to the end of the current UTC month. quarter: to the end of the third month after it. |
scenario | Up to 10 changes. See Scenarios. With GET, send it as URL-encoded JSON. |
POST takes the same four fields as a JSON body. An unknown field returns 400.
How RouterPlus projects each group's daily spend:
method | When | How |
|---|---|---|
weekday_trend_14d | 14 or more days of history | A least-squares line through the last 14 days, times each weekday's factor from the last 28 days, clipped at zero. |
trailing_mean | 3 to 13 days of history | The mean daily spend of those days. No weekday pattern, no trend. |
insufficient_history | Fewer than 3 days | No projection. projected_usd, low_usd and high_usd are null. |
low_usd and high_usd are about a 90% range. History counts from the first day with spend.
Response:
| Field | Notes |
|---|---|
group_by, label, horizon | The parameters used. |
as_of | Today, UTC date. |
periods | The months of the horizon: {period, start, end}. |
method | {trend_days: 14, pattern_days: 28, min_history_days: 3, band: "90%", descriptions}. |
total | The org's projection. See the projection fields below. |
data | One row per group, largest first, at most 200. Each row has the group field (model, workspace_id, key_id or value), name for workspaces and keys, and the projection fields. |
crossings | Budgets and key limits projected to run out before the end of the horizon, earliest first. |
scenario | Null, or the scenario's result. |
daily | {start, usd}: the org's actual spend per day, from 42 days before today. |
Projection fields: spent_so_far_usd (this month, today included), projected_usd, low_usd, high_usd, method, history_days, history_too_short, and periods (per month: period, start, end, spent_usd, projected_usd, low_usd, high_usd).
A crossing:
| Field | Notes |
|---|---|
kind | guardrail_budget or key_limit. |
| Budget fields | guardrail_id, budget_id, target_kind (group for a shared budget), target_id, model, provider, spent_known. |
| Key limit fields | key_id, name, key_prefix, limit_kind (limit or monthly_cap). |
period, limit_usd, spent_usd | The limit and its spend in the current period. |
crosses_on | The first UTC date the projection reaches the limit. Spend resets at each period start. |
already_exceeded | true when spend has already reached the limit. crosses_on is then today. |
method, history_days | How that scope's spend was projected. |
Scenarios
A scenario changes the history that RouterPlus fits the projection to. It never changes spend so far.
| Change | Does |
|---|---|
{"scale": n} | Multiplies all traffic by n (0 to 100). |
{"scale": n, "workspace_id": "..."} | Multiplies one workspace's traffic. Use model or key_id in place of workspace_id for one model or key. Name at most one. |
{"swap": {"from": "a", "to": "b"}} | Moves model a's traffic to model b. It prices the last 14 days of a's tokens on b's cheapest catalog route that your org guardrails allow. |
curl https://dev2-api.routerplus.com/v1/activity/forecast \
-H "Authorization: Bearer $TM_MANAGEMENT_KEY" \
-H "content-type: application/json" \
-d '{"horizon": "month", "scenario": [{"scale": 2}, {"swap": {"from": "google/gemini-3.1-pro", "to": "google/gemini-3.5-flash"}}]}'scenario in the response has:
changes: each change. A swap addsprovider,cost_ratioandtokens_basis_days(14), and anotewhenahad no spend in the last 14 days.total: the projection fields anddelta_usd, the change from the base projection.data: per group,projected_usd,low_usd,high_usd,delta_usdandmethod.crossings: the crossings under the scenario.
A swap returns 400 when the target model is not allowed by your org guardrails, has no catalog price on an allowed route, or when one model is swapped twice.
Savings
GET /v1/activity/savings reprices your traffic on RouterPlus credits for a window. For each model and provider, it shows what the same tokens would cost on a cheaper route of the same model, and on cheaper models of a similar quality tier. BYOK traffic is left out.
curl "https://demo-api.routerplus.com/v1/activity/savings?days=30" \
-H "Authorization: Bearer $TM_MANAGEMENT_KEY"| Query | Notes |
|---|---|
days | An integer from 1 to 90. Default 30. The window ends today. |
The rules:
- A suggestion must save at least 1% of the baseline.
- A suggestion meets the guardrails of the keys that sent the traffic.
- A model of a similar tier has an Artificial Analysis intelligence index at most 3 points below the current model's. A model with no score is not compared. The answer counts those models.
- Each row lists at most 3 other models.
- The figures reprice token counts. They do not show that answers would be as good. Each row with other models includes the body of an eval to test them.
Response:
| Field | Notes |
|---|---|
days, from, to | The window. |
scores_status | Whether benchmark scores are loaded. unavailable when the server has no model catalog. |
tier_points | 3. |
summary | spend_usd, route_saving_usd, model_saving_usd. |
data | One row per model and provider, largest saving first. |
notes | Plain-text notes on what the figures mean. |
source, source_url | Artificial Analysis, https://artificialanalysis.ai/. |
A data row:
| Field | Notes |
|---|---|
model, provider, requests, spend_usd | The traffic. |
tokens | input, cached_input, cache_write, output, reasoning. |
baseline_usd, baseline_basis | The cost the savings compare against. catalog_price when the current route has a catalog price, else spend (what you were charged). |
intelligence_index | The model's score, or null. |
cheapest_route | Null, or {provider, cost_usd, saving_usd, saving_percent, confidence, note} for the same model on another provider. confidence is high, or medium against spend. |
route_note | Why there is no cheapest_route, or null. |
alternative_models | Up to 3 of {model, provider, intelligence_index, index_difference, cost_usd, saving_usd, saving_percent, confidence, note}. confidence is medium for an equal or higher score, low for a lower one. |
alternatives_note | Why there are no alternatives, or how many models had no score. |
eval | Null, or {method: "POST", path: "/v1/evals", body, note}: an eval of the current and other models, with a pairwise judge. Replace cases with 20 or more real prompts. |
best_saving_usd | The larger of the route saving and the best model saving. |
Labels
Label a request in one of three ways:
- The header
x-tm-labels: team=search,feature=autocomplete(alsox-routerplus-labels). - The body field
trace: an object of string values. RouterPlus removes it before the request reaches a provider. - Labels set on the key (
PATCH /v1/keys/<id> {labels}).
Rules:
- For one name, a request's own label wins over the key's.
- At most 20 labels. Names are lowercase letters, digits,
_,.and-, up to 40 characters. Values are up to 200 characters. - RouterPlus adds
task(the kind of request). x-tm-finance-categorysets the request's finance category.
Security report
| Route | Does |
|---|---|
GET /v1/security/keys | Lists every active API key with flags. summary counts each flag. |
| Flag | True when |
|---|---|
no_limit | The key has no spend limit. |
no_expiry | The key never expires. |
idle | No request in 30 days, or no request ever and the key is older than 7 days. |
expiring_soon | The key expires within 7 days. |
no_ip_allowlist | The key works from any address. |
rotated | A rotation replaced the key, and it still works in its grace period. |
Each key also has last_used_at, created_at, expires_at and usage_30d_usd.
Audit log
| Route | Does |
|---|---|
GET /v1/audit | Returns {source, at, actor, action, subject} rows, newest first. |
sourceisgovernance,identity,connectionorrouting_policy.- Query:
since,until,action,actor,limit(at most 500, default 100). - Grant changes write the actions
grant.request,grant.approve,grant.rejectandgrant.cancel.
SLA report
| Route | Does |
|---|---|
GET /v1/sla/report?month=YYYY-MM | Your org's availability in one UTC month. |
Without month, it reports the current month up to now. A month in the future returns 400.
The report counts requests. A request's result is its last attempt:
| Class | The last attempt |
|---|---|
good | Completed. |
buyer_caused | Was cancelled by the client, ended with HTTP 400, 413 or 429, or was refused while the org's configuration changed. |
upstream_quota | Hit a provider pool's quota. |
failed | Any other failure. |
pending | Is not settled yet. Left out of the other counts. |
{"data": {"month": "2026-09", "period_start": "2026-09-01T00:00:00.000Z", "period_end": "2026-10-01T00:00:00.000Z", "complete": true,
"requests": {"total": 75, "good": 57, "buyer_caused": 4, "upstream_quota": 1, "failed": 13, "pending": 1},
"availability_percent": 82.6667,
"degraded": {"bucket_minutes": 5, "min_requests": 20, "failed_percent_over": 5, "minutes": 5, "period_minutes": 43200,
"minutes_percent_available": 99.9884, "buckets": [{"start": "2026-09-10T12:05:00.000Z", "requests": 20, "failed": 2}]},
"credit": null}}| Field | Meaning |
|---|---|
availability_percent | (total - failed) / total x 100. Null with no requests. |
degraded.minutes | 5 minutes for each 5-minute bucket with at least 20 requests and more than 5% failed. |
credit | Null unless your agreement sets credit tiers. Then it is {tiers, tier}, where tier is the tier for this month, or null. |
Configuration backup
| Route | Does |
|---|---|
GET /v1/config/export | Exports the configuration. Add ?include_secrets=true to include secrets. |
POST /v1/config/import | {mode, dry_run?, config}, where config is an export. |
The export:
{"version": 1, "exported_at": "...", "guardrails": [...], "presets": [...], "workspaces": [...],
"notification_channels": [...], "broadcast_destinations": [...]}Secrets are written as "__secret__" unless you add ?include_secrets=true.
Import modes:
| Value | Does |
|---|---|
mode: "merge" | Creates what is missing and updates what matches. Workspaces and guardrails match by name, presets by slug, channels and destinations by id, then by address or name. |
mode: "replace" | Does what merge does, then deletes the guardrails and presets that the file does not list. |
dry_run: true | Returns the same counts and changes nothing. |
A "__secret__" value keeps the stored secret. RouterPlus skips a record that needs a secret you did not send.
The response:
{"data": {"mode": "merge", "dry_run": false,
"counts": {"workspaces": {"created": 1, "updated": 0, "deleted": 0}, "guardrails": {...}, "assignments": {...},
"presets": {...}, "notification_channels": {...}, "broadcast_destinations": {...}},
"skipped": [{"kind": "notification_channels", "ref": "slack:...", "reason": "..."}]}}For agents: /docs/api-management.md and /llms.txt