Console
API reference

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.

bash
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/key returns 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

json
{"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.

StatusMeaning
400The input is not valid. message names the field.
401No valid key.
403The key is valid but cannot do this. For example, an API key cannot approve a grant.
404No such record in your organization, or no such route.
405The route does not take this method.
409A conflict: a record exists, a version is stale, or the record is in the wrong state.
413The body is larger than 256 KiB.

Key status (API key)

RouteDoes
GET /v1/keyReturns the calling API key's own status. Authenticate with Authorization: Bearer tm_vk_....
json
{"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}}
  • usage is the spend in the current limit period. With no limit_reset, it is the spend over the key's life.
  • While an active grant raises the key's limit, limit is the granted value. The answer then also has base_limit (the key's own limit) and grant_id.
  • An expired key returns 401 key_expired.

API keys

RouteDoes
GET /v1/keysLists active keys. Query: workspace_id, include_disabled=true.
POST /v1/keysCreates a key. Returns 201 with key (the raw key) and data.
GET /v1/keys/:idReads one key.
PATCH /v1/keys/:idChanges the fields you send.
DELETE /v1/keys/:idDisables the key. It stops working at once.
POST /v1/keys/:id/rotateCreates 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

FieldTypeNotes
namestringRequired. 1 to 80 characters.
workspace_idUUIDDefault: the default workspace.
principal_idUUIDDefault: the workspace's service principal.
limitnumber or nullUSD per limit_reset period.
limit_resetstring or nulldaily, weekly, monthly, or null for no reset.
include_byok_in_limitbooleanCount BYOK inference cost in the limit.
expires_atISO time or nullMust be in the future. The key stops working then.
ip_allowlistarray or nullCIDR ranges or addresses. Null allows any address.
labelsobjectAt most 20. Keys: a-z 0-9 _ . -, 1 to 40 characters. Values: at most 200 characters.
finance_categorystring or null1 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_from set to the old key's id.
  • A disabled or expired key cannot be rotated (409).

Key object

json
{"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

RouteDoes
GET /v1/management-keysLists management keys.
POST /v1/management-keys{name, expires_at?}. Returns 201 with key (shown once).
DELETE /v1/management-keys/:idDisables 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.

RouteDoes
GET /v1/workload-identitiesLists identities.
POST /v1/workload-identitiesCreates one. Returns 201.
DELETE /v1/workload-identities/:idDeletes one.
FieldNotes
nameRequired. 1 to 100 characters.
workspace_idRequired.
principal_idDefault: the workspace's service principal.
issuerAn https URL, such as https://token.actions.githubusercontent.com.
audienceThe token's aud.
subject_patternThe token's sub. * matches any run of characters.
required_claimsClaim names and the exact values they must have. At most 20.
key_ttl_seconds300 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.

RouteDoes
GET /v1/guardrailsLists guardrails with their assignments.
POST /v1/guardrails{name, description?, ...rules}. Returns 201.
GET /v1/guardrails/:idReads one guardrail.
PATCH /v1/guardrails/:idReplaces the rules you send. version goes up by 1.
DELETE /v1/guardrails/:idDeletes the guardrail and its assignments.
POST /v1/guardrails/:id/assignments{target_kind, target_id}. Returns 201.
DELETE /v1/guardrails/:id/assignments/:kind/:targetRemoves one assignment. URL-encode an email.
GET /v1/guardrails/:id/statusSpend of each budget in its current period.
GET /v1/guardrails/eventsRequests a guardrail blocked, redacted or flagged.

Rules

FieldType
allowed_models, blocked_modelsModel patterns. * matches any run of characters.
allowed_providers, blocked_providersProvider names.
zdrboolean. Only providers that keep no data.
data_collection"deny": only providers that do not train on data.
allowed_regionsRegion codes such as us, eu, in.
blocked_infrastructureClouds such as aws, gcp.
budgetsUp 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_budgetsOne budget, in the short form.
rpm, tpm, max_output_tokensIntegers.
server_toolsallow or block.
content_rulesUp to 50 of {id?, kind, preset?, pattern?, flags?, action, label?}. kind: pii, regex, injection. action: redact, block, flag.
on_scan_limitallow or block, for text too long to scan in full.
overridableboolean. 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_kindtarget_id
orgYour org id, or leave it out.
workspaceA workspace id.
memberA member's email.
keyAn 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:

json
{"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

QueryNotes
since, untilTime range.
actionblocked, redacted or flagged.
key_idOne key.
limitAt 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.

bash
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"}'
RouteDoesWho may call
GET /v1/grantsLists grants, newest first.Management key. An API key sees only the grants it requested.
POST /v1/grantsRequests a grant. Returns 201 with the grant in pending status.Management key, or an API key for itself.
GET /v1/grants/:idReads 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

FieldNotes
targetRequired. What the grant changes. See the table below.
changeRequired. {"limit_usd": n} for budget and key_limit grants. {"allow_model": "<id or pattern>"} for guardrail grants.
starts_atISO time. Default: now. It cannot be in the past (5 minutes of clock skew is allowed).
expires_atRequired. ISO time after now and after starts_at. At most 90 days after starts_at.
reasonRequired. 1 to 500 characters.
requested_byManagement key only. The email of an active member to record as the requester.

target:

typeFieldsChanges
budgetguardrail_id, budget_id, and optionally target_kind and target_idRaises 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_limitkey_idRaises an API key's limit to change.limit_usd. The key must have a limit.
guardrailguardrail_id, and optionally target_kind and target_idLets 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_usd is 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_limit grant for its own key only.
  • A budget grant 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 guardrail grant only on a guardrail that applies to the key. The grant covers that key only.
  • An API key cannot set requested_by.

Grant object

json
{"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": "..."}}
  • status is pending, approved, rejected or cancelled.
  • state adds the time window: pending, scheduled (approved, not started), active, expired, rejected or cancelled.
  • base_limit_usd is the limit when the grant was requested. It is null for guardrail grants.
  • GET /v1/grants/:id adds events: each state change with action, actor, detail and created_at.

List query

QueryNotes
statuspending, approved, rejected or cancelled.
statepending, scheduled, active, expired, rejected or cancelled.
target_typebudget, key_limit or guardrail.
key_idGrants on that key's limit, or guardrail grants scoped to that key.
guardrail_idGrants on one guardrail.
limitAt 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_usd and grant_id per budget.
  • Key reads and GET /v1/key show the granted limit, base_limit and grant_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 model or provider caps every grant that raises a budget or key limit with the same period. A raise above the cap returns 409 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 typeWhen
400 invalid_expiryexpires_at is not after now and after starts_at.
400 grant_too_longThe grant lasts more than 90 days.
400 grant_not_raisechange.limit_usd is not more than the current limit.
403 forbiddenAn API key tried to approve or reject, set requested_by, or request a grant outside its own key.
404The guardrail, budget, assignment, key, member or grant is not in your org.
409 hard_limitThe guardrail is a hard limit, or the raise is above the org's hard limit.
409 no_limitThe key has no limit to raise.
409 grant_not_pendingApprove or reject on a grant that is not pending.
409 grant_not_openCancel on a grant that is rejected or cancelled.
409 grant_expiredThe grant's expires_at has passed.

Presets

A preset is a versioned request template. Call it with "model": "@preset/<slug>" or "@preset/<slug>@<version>".

RouteDoes
GET /v1/presetsLists the latest version of each preset. Query: workspace_id.
POST /v1/presets{slug, description?, workspace_id?, template}. Creates version 1.
GET /v1/presets/:slugThe latest version. Query version=N for an older one.
PUT /v1/presets/:slug{template, description?, expected_version}. Creates the next version.
DELETE /v1/presets/:slugDeletes every version.
  • A slug is 1 to 80 of a-z 0-9 _ -, starting with a letter or digit.
  • POST returns 409 preset_exists when the slug is in use.
  • PUT returns 409 version_conflict when expected_version is not the latest version.

Template

FieldType
model or modelsSend one, not both. models lists 1 to 5 models, tried in order.
providerA provider routing object, as in a request.
defaultsParameters used when the request does not set them.
overridesParameters that replace the request's values.
system_promptUp to 20,000 characters. Put before the request's own system text.
guardrail_idA guardrail in your org.

Parameters in defaults and overrides:

ParameterRange
temperature0 to 2
top_p0 to 1
top_k1 to 32768
max_tokens, max_completion_tokens1 to 1,000,000
frequency_penalty, presence_penalty-2 to 2
seedinteger
stopa string or up to 4 strings
reasoning_effortminimal, low, medium, high, xhigh
response_formatobject
parallel_tool_calls, streamboolean

How a preset combines with a request

  1. 01
    overrides beat the request. The request beats defaults.
  2. 02
    The template's model or models replace the request's.
  3. 03
    A request's provider object can only narrow the template's. only keeps the providers in both. ignore adds. allow_fallbacks: false, zdr: true and data_collection: "deny" always win. The request's sort and order apply only when the template sets none.
  4. 04
    On /v1/messages, a preset sets temperature, top_p, top_k, max_tokens, stop (as stop_sequences) and stream. The other parameters apply on the OpenAI surface only.

Workspaces

RouteDoes
GET /v1/workspacesLists workspaces.
POST /v1/workspaces{name, finance_category?, default_sort?}. Returns 201.
PATCH /v1/workspaces/:idname, disabled, finance_category, default_sort, route_policy_id.
POST /v1/workspaces/:id/clone{name, include}. Returns 201.
GET /v1/workspaces/:id/membersLists workspace members.
PUT /v1/workspaces/:id/members{email, role}. Adds a member or changes the role.
GET /v1/workspaces/:id/members/:emailReads one member.
PUT /v1/workspaces/:id/members/:email{role}.
DELETE /v1/workspaces/:id/members/:emailRemoves a member.
  • default_sort: routing_cost, price, throughput or latency.
  • role: admin, member or viewer.
  • 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

RouteDoes
GET /v1/orgReturns org_id, name, default_sort, request_log, request_log_until, retention_days and hipaa_mode.
PATCH /v1/orgChanges the fields below.
FieldNotes
default_sortAs for workspaces, or null.
request_logon 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_days1 to 3650, or null for the defaults. See Retention.
hipaa_modetrue 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.

RecordsDefault limit
Request labels and sessions400 days
Guardrail events400 days
MCP tool call records400 days
Eval answers and judge results90 days after the eval finished
Batch filesAt their expires_at
Slack agent thread memoryEmptied 7 days after the last message, deleted 7 days later
OAuth codes and SSO sign-in states1 day after they expire
Sign-in log400 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 in x-tm-guardrails is 00000000-0000-4000-8000-000000000074. RouterPlus refuses a model with no route that meets it.
  • RouterPlus does not log requests. The org's request_log setting 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: true returns 409 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.

RouteDoes
GET /v1/notifications/channelsLists channels.
POST /v1/notifications/channels{kind, config, enabled?, thresholds?}. Returns 201.
PATCH /v1/notifications/channels/:idChanges the fields you send. kind cannot change.
DELETE /v1/notifications/channels/:idDeletes the channel.
POST /v1/notifications/channels/:id/testSends a sample alert. Returns {ok, status?, error?}.
kindconfig
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.
  • thresholds are 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.

RouteDoes
GET /v1/broadcast/destinationsLists destinations.
POST /v1/broadcast/destinations{name?, kind, config, enabled?, sampling?, privacy?, filter?}. Returns 201.
PATCH /v1/broadcast/destinations/:idChanges the fields you send. kind cannot change.
DELETE /v1/broadcast/destinations/:idDeletes the destination.
POST /v1/broadcast/destinations/:id/testSends one sample trace. Returns {ok, status}.
kindconfig
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.

FieldNotes
sampling0 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

RouteDoes
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

RouteDoes
GET /v1/activityUsage per group.
GET /v1/activity/labels?label=<key>Requests grouped by the value of one request label.
GET /v1/activity/sessionsRequests grouped by session id.
GET /v1/activity/cachePrompt caching per model and provider.
GET /v1/activity/cache/adviceWhat in your traffic costs cache reuse, with the numbers and the fix.
GET /v1/activity/byokRequests on your own provider connections, per connection and model.
GET /v1/activity/forecastProjected spend to the end of the month or quarter. See Spend forecast.
POST /v1/activity/forecastThe same forecast, with the parameters in the body.
GET /v1/activity/savingsWhat your traffic would cost on cheaper routes or models. See Savings.

GET /v1/activity takes:

QueryNotes
from, toUTC dates, both included. Default: the last 30 days. At most 366 days.
group_byday (default), model, provider, key, workspace, member, finance_category.
key_id, workspace_id, model, providerFilters.

The label, session, cache and BYOK reports take from and to too.

Response rows:

ReportEach row has
/v1/activityThe 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/labelsvalue, requests, attempts, usage_usd and the token counts.
/v1/activity/sessionssession_id, requests, usage_usd, models, first_seen, last_seen. Query limit: at most 500.
/v1/activity/cacheinput_tokens, cached_input_tokens, cache_write_tokens, cache_hit_rate, estimated_savings_usd.
/v1/activity/byokrequests, list_cost_usd, negotiated_discount_percent, negotiated_cost_usd (the list cost less the discount set with PATCH /v1/connections/<id>).
  • cache_hit_rate is cached input tokens divided by input tokens.
  • estimated_savings_usd is the cached tokens times the difference between the input price and the cached input price.

/v1/activity/cache/advice returns these findings:

FindingWhen
low_hit_rateA model with 1M+ input tokens and a hit rate under 20%, with the most it could save.
cache_writes_unreadCache writes that no later request read.
no_session_idsOver 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.

bash
curl "https://demo-api.routerplus.com/v1/activity/forecast?group_by=workspace&horizon=quarter" \
  -H "Authorization: Bearer $TM_MANAGEMENT_KEY"
ParameterNotes
group_bymodel (default), workspace, key or label.
labelRequired with group_by=label. A label key: 1 to 40 of a-z 0-9 _ . -.
horizonmonth (default): to the end of the current UTC month. quarter: to the end of the third month after it.
scenarioUp 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:

methodWhenHow
weekday_trend_14d14 or more days of historyA least-squares line through the last 14 days, times each weekday's factor from the last 28 days, clipped at zero.
trailing_mean3 to 13 days of historyThe mean daily spend of those days. No weekday pattern, no trend.
insufficient_historyFewer than 3 daysNo 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:

FieldNotes
group_by, label, horizonThe parameters used.
as_ofToday, UTC date.
periodsThe months of the horizon: {period, start, end}.
method{trend_days: 14, pattern_days: 28, min_history_days: 3, band: "90%", descriptions}.
totalThe org's projection. See the projection fields below.
dataOne 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.
crossingsBudgets and key limits projected to run out before the end of the horizon, earliest first.
scenarioNull, 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:

FieldNotes
kindguardrail_budget or key_limit.
Budget fieldsguardrail_id, budget_id, target_kind (group for a shared budget), target_id, model, provider, spent_known.
Key limit fieldskey_id, name, key_prefix, limit_kind (limit or monthly_cap).
period, limit_usd, spent_usdThe limit and its spend in the current period.
crosses_onThe first UTC date the projection reaches the limit. Spend resets at each period start.
already_exceededtrue when spend has already reached the limit. crosses_on is then today.
method, history_daysHow that scope's spend was projected.

Scenarios

A scenario changes the history that RouterPlus fits the projection to. It never changes spend so far.

ChangeDoes
{"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.
bash
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 adds provider, cost_ratio and tokens_basis_days (14), and a note when a had no spend in the last 14 days.
  • total: the projection fields and delta_usd, the change from the base projection.
  • data: per group, projected_usd, low_usd, high_usd, delta_usd and method.
  • 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.

bash
curl "https://demo-api.routerplus.com/v1/activity/savings?days=30" \
  -H "Authorization: Bearer $TM_MANAGEMENT_KEY"
QueryNotes
daysAn 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:

FieldNotes
days, from, toThe window.
scores_statusWhether benchmark scores are loaded. unavailable when the server has no model catalog.
tier_points3.
summaryspend_usd, route_saving_usd, model_saving_usd.
dataOne row per model and provider, largest saving first.
notesPlain-text notes on what the figures mean.
source, source_urlArtificial Analysis, https://artificialanalysis.ai/.

A data row:

FieldNotes
model, provider, requests, spend_usdThe traffic.
tokensinput, cached_input, cache_write, output, reasoning.
baseline_usd, baseline_basisThe cost the savings compare against. catalog_price when the current route has a catalog price, else spend (what you were charged).
intelligence_indexThe model's score, or null.
cheapest_routeNull, 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_noteWhy there is no cheapest_route, or null.
alternative_modelsUp 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_noteWhy there are no alternatives, or how many models had no score.
evalNull, 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_usdThe 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 (also x-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-category sets the request's finance category.

Security report

RouteDoes
GET /v1/security/keysLists every active API key with flags. summary counts each flag.
FlagTrue when
no_limitThe key has no spend limit.
no_expiryThe key never expires.
idleNo request in 30 days, or no request ever and the key is older than 7 days.
expiring_soonThe key expires within 7 days.
no_ip_allowlistThe key works from any address.
rotatedA 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

RouteDoes
GET /v1/auditReturns {source, at, actor, action, subject} rows, newest first.
  • source is governance, identity, connection or routing_policy.
  • Query: since, until, action, actor, limit (at most 500, default 100).
  • Grant changes write the actions grant.request, grant.approve, grant.reject and grant.cancel.

SLA report

RouteDoes
GET /v1/sla/report?month=YYYY-MMYour 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:

ClassThe last attempt
goodCompleted.
buyer_causedWas cancelled by the client, ended with HTTP 400, 413 or 429, or was refused while the org's configuration changed.
upstream_quotaHit a provider pool's quota.
failedAny other failure.
pendingIs not settled yet. Left out of the other counts.
json
{"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}}
FieldMeaning
availability_percent(total - failed) / total x 100. Null with no requests.
degraded.minutes5 minutes for each 5-minute bucket with at least 20 requests and more than 5% failed.
creditNull unless your agreement sets credit tiers. Then it is {tiers, tier}, where tier is the tier for this month, or null.

Configuration backup

RouteDoes
GET /v1/config/exportExports the configuration. Add ?include_secrets=true to include secrets.
POST /v1/config/import{mode, dry_run?, config}, where config is an export.

The export:

json
{"version": 1, "exported_at": "...", "guardrails": [...], "presets": [...], "workspaces": [...],
 "notification_channels": [...], "broadcast_destinations": [...]}

Secrets are written as "__secret__" unless you add ?include_secrets=true.

Import modes:

ValueDoes
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: trueReturns 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:

json
{"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