Management API Keys

Programmatically mint FreeRouter keys, store provider keys, and attach routing rules — one fully-routed key per customer, automated rotation, usage-driven revocation.

What they are

A management key (fr_mgmt_…) is a workspace-scoped admin credential. It authorizes the management API on the app host, which lists, creates, updates, and revokes your workspace's FreeRouter keys (fr_live_…), stores BYOK provider keys, and attaches routing rules — and the MCP server at https://api.freerouter.com/mcp.

Management keys cannot run inference — presenting one to api.freerouter.com/v1 returns 401. They can authenticate POST /mcp on that same host. Conversely, a FreeRouter key cannot manage keys — presenting one to the management API or to /mcp returns 401. The two key types live in separate tables and are never interchangeable.

Admin-only — keep them server-side A management key can mint FreeRouter keys that spend against your provider keys. Store it like any server secret, and revoke it from Settings if it leaks.

Creating a management API key

1
Open the profile menu → Settings
In the dashboard, click your profile menu (top right) and choose Settings. Management keys live here — deliberately not in the dashboard section tabs.
2
Management API Keys → Create management key
Give it a label (e.g. "Backend key manager") and create it. Copy the fr_mgmt_… secret — it is shown once and never again.

Use cases

  • SaaS applications — mint a unique, fully-routed FreeRouter key per customer instance (provider keys + routing included).
  • Key rotation — regularly rotate FreeRouter keys for security compliance.
  • Usage monitoring — track keys and revoke ones that exceed your limits.
  • Agents / IDEs — connect Cursor or Claude to the MCP server with the same management key.
MCP The same fr_mgmt_… Bearer token authenticates https://api.freerouter.com/mcp. Use REST from backends; use MCP from agents. Read-only keys block write tools on both surfaces.

Read-only reporting keys

Flag a management key read-only to use it as a reporting key: it keeps full GET access — key metadata, provider inventory, routing, plus the usage and account endpoints below — but every write (POST/PATCH/PUT/DELETE on keys, providers, or routing) returns 403. Hand one to a reporting job or a contractor dashboard without worrying about key creation or revocation.

Set the flag with the Read-only reporting key checkbox when you create the key in Settings, or flip it later with the Make read-only / Allow writes button on each key. The flag can only be changed from the dashboard — the API itself offers no way for a key to escalate its own privileges. Read-only keys still cannot run inference.

Base URL & auth

All management endpoints live under /api/v1 on the app host and take the management key as a Bearer token.

https://app.freerouter.com/api/v1
Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY

Management keys are not subject to IP allowlists; those restrict FreeRouter keys only. Keep management keys in server-side secrets and use read-only keys where reporting is all you need.

How identifiers work

Two different ids show up on this API. Mixing them up is the usual 400.

  • Gateway identifier — a stable slug such as openrouter or vercel. Discover them with GET /api/v1/gateways. Pass this as gateway when you store a provider key.
  • Provider key UUID — the id returned when you list or create a provider. This is the attach handle. Routing targets take provider_key_id (that UUID), never the gateway slug. FreeRouter fills gateway on the stored rule from the provider row.

A typical setup is three calls: catalog → store a provider → mint a key with routing (or PUT routing onto an existing key).

curl https://app.freerouter.com/api/v1/gateways \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

curl https://app.freerouter.com/api/v1/providers \
  -X POST \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "gateway": "openrouter", "name": "Primary", "key": "$OPENROUTER_API_KEY" }'

curl https://app.freerouter.com/api/v1/keys \
  -X POST \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Customer Instance Key",
    "routing": {
      "strategy": "priority",
      "targets": [{ "provider_key_id": "<id from POST /providers>" }]
    }
  }'

List gateways — GET /api/v1/gateways

Canonical gateway identifiers, in the order the proxy registers them. Use id as gateway when creating a provider key. routable: true means the gateway can be a chat routing target; decisions: true means it can be a target on a systemone key (TypeSafe and OpenAI are Decisions-only — routable: false, so a chat key routed at them answers 501). Cloudflare lists the extra config fields it needs.

curl https://app.freerouter.com/api/v1/gateways \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: […] }:

{
  "data": [
    { "id": "openrouter", "name": "OpenRouter", "routable": true, "decisions": true },
    { "id": "vercel", "name": "Vercel AI Gateway", "routable": true, "decisions": true },
    {
      "id": "cloudflare",
      "name": "Cloudflare AI Gateway",
      "routable": true,
      "decisions": true,
      "config_fields": [
        { "name": "account_id", "required": true },
        { "name": "gateway_id", "required": false, "default": "default" }
      ]
    },
    { "id": "ramprouter", "name": "Ramp Router", "routable": true, "decisions": true },
    { "id": "together", "name": "Together AI", "routable": true, "decisions": false },
    { "id": "orcarouter", "name": "OrcaRouter", "routable": true, "decisions": false },
    { "id": "requesty", "name": "Requesty", "routable": true, "decisions": true },
    { "id": "concentrate", "name": "Concentrate", "routable": true, "decisions": false },
    { "id": "llmgateway", "name": "LLM Gateway", "routable": true, "decisions": false },
    { "id": "baseten", "name": "Baseten", "routable": true, "decisions": false },
    { "id": "darkbloom", "name": "Darkbloom", "routable": true, "decisions": false },
    { "id": "sail", "name": "Sail", "routable": true, "decisions": false },
    { "id": "fireworks", "name": "Fireworks AI", "routable": true, "decisions": false },
    { "id": "morph", "name": "Morph", "routable": true, "decisions": false },
    { "id": "cheaperinference", "name": "Cheaper Inference", "routable": true, "decisions": false },
    { "id": "celeris", "name": "Celeris", "routable": true, "decisions": true },
    { "id": "saygm", "name": "SayGM", "routable": true, "decisions": false },
    { "id": "experiential", "name": "Experiential Labs", "routable": true, "decisions": false },
    { "id": "nanogpt", "name": "NanoGPT", "routable": true, "decisions": false },
    { "id": "typesafe", "name": "TypeSafe", "routable": false, "decisions": true },
    { "id": "openai", "name": "OpenAI", "routable": false, "decisions": true }
  ]
}

List provider keys — GET /api/v1/providers

Metadata only — never secrets. Each id is the UUID you pass as provider_key_id on a routing rule. Filter with ?gateway=openrouter (the identifier from the catalog) when a workspace has several keys for the same gateway; distinguish those rows by label.

curl https://app.freerouter.com/api/v1/providers \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"
curl "https://app.freerouter.com/api/v1/providers?gateway=openrouter" \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: […] }:

{
  "data": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "gateway": "openrouter",
      "label": "Primary",
      "key_last4": "a1b2",
      "config": {},
      "created_at": "2026-09-01T09:00:00.000Z",
      "disabled_at": null
    }
  ]
}

Store a provider key — POST /api/v1/providers

Body: gateway (required, a catalog id), key (or secret; a value over 2000 characters is refused with 400), optional name (or label; cut to 120 characters), optional config. The secret is encrypted at rest and never returned — keep data.id as the attach handle. Cloudflare requires config.account_id; config.gateway_id defaults to default. A provider's free-text notes are dashboard-only.

curl https://app.freerouter.com/api/v1/providers \
  -X POST \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "gateway": "openrouter",
    "name": "Primary",
    "key": "$OPENROUTER_API_KEY"
  }'
curl https://app.freerouter.com/api/v1/providers \
  -X POST \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "gateway": "cloudflare",
    "key": "$CLOUDFLARE_API_TOKEN",
    "config": { "account_id": "your-account-id", "gateway_id": "default" }
  }'

Response — 201. An unknown gateway, a missing secret, or Cloudflare without account_id returns 400.

{
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "gateway": "openrouter",
    "label": "Primary",
    "key_last4": "a1b2",
    "config": {},
    "created_at": "2026-09-05T12:00:00.000Z",
    "disabled_at": null
  }
}

Get a provider key — GET /api/v1/providers/:id

curl https://app.freerouter.com/api/v1/providers/PROVIDER_ID \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with the same metadata as listing (no secret). Unknown ids return 404 { error: "Provider key not found." }.

Update a provider key — PATCH /api/v1/providers/:id

Rename with name (or label); disable with disabled: true, re-enable with disabled: false. Disable keeps routing references so re-enabling restores them; delete strips the id from every rule.

curl https://app.freerouter.com/api/v1/providers/PROVIDER_ID \
  -X PATCH \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Backup OpenRouter", "disabled": false }'

Response — 200 with the updated metadata. An empty body returns 400; a non-boolean disabled returns 400; unknown ids return 404.

Delete a provider key — DELETE /api/v1/providers/:id

Removes the stored key and strips its UUID from every routing rule in the workspace. A rule left with zero targets 402s until you re-point it.

curl -X DELETE https://app.freerouter.com/api/v1/providers/PROVIDER_ID \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200:

{ "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "deleted": true } }

List keys — GET /api/v1/keys

Returns the workspace's FreeRouter keys (metadata only — never secrets), including each key's routing rule. Most recent 100. Paginate with ?offset=N.

curl https://app.freerouter.com/api/v1/keys \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"
curl "https://app.freerouter.com/api/v1/keys?offset=100" \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: […] } (metadata only — never secrets):

{
  "data": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "label": "Customer Instance Key",
      "key_prefix": "fr_live_abc12345",
      "api_shape": "openai",
      "companion_ads": false,
      "keyterms": false,
      "allowed_ips": [],
      "steering_message": null,
      "failover_ttfb_ms": null,
      "upstream_timeout_ms": null,
      "effective_timeouts": { "failover_ttfb_ms": 5000, "upstream_timeout_ms": 60000 },
      "created_at": "2026-09-03T12:00:00.000Z",
      "last_used_at": null,
      "revoked_at": null,
      "routing": {
        "strategy": "priority",
        "targets": [
          { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" }
        ],
        "model_remaps": [],
        "decisioner": null
      }
    },
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "label": null,
      "key_prefix": "fr_live_def67890",
      "api_shape": "anthropic",
      "companion_ads": false,
      "keyterms": false,
      "allowed_ips": [],
      "steering_message": null,
      "failover_ttfb_ms": null,
      "upstream_timeout_ms": null,
      "effective_timeouts": { "failover_ttfb_ms": 5000, "upstream_timeout_ms": 60000 },
      "created_at": "2026-09-04T09:30:00.000Z",
      "last_used_at": "2026-09-04T10:15:00.000Z",
      "revoked_at": null,
      "routing": { "strategy": "priority", "targets": [], "model_remaps": [], "decisioner": null }
    }
  ]
}

Create a key — POST /api/v1/keys

Body: name (or label; longer than 120 characters is cut to 120, not refused), an optional api_shape (openai default, openrouter, anthropic, google, systemone), optional routing (same shape as PUT /keys/:id/routing), optional companion_ads: true to turn on Companion Ads, optional keyterms: true to turn on Monetizable Keyterms (stored as false unless the operator has set FR_KEYTERMS_ENABLED on the app — read the response), an optional allowed_ips array of IPv4/IPv6 addresses or CIDR ranges (see IP allowlists; a comma-separated string works too; at most 100 entries of 64 characters each), an optional steering_message appended to the system prompt of every request on the key (see Steering message; up to 2000 characters), and optional failover_ttfb_ms / upstream_timeout_ms (see Timeouts). The new key's secret comes back once as data.key — store it; it is never returned again. If routing is invalid the unused key is rolled back. A systemone key's routing targets must be Decisions hosts: TypeSafe, OpenRouter, Requesty, Cloudflare, OpenAI, Vercel, Celeris, or Ramp Router.

curl https://app.freerouter.com/api/v1/keys \
  -X POST \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Customer Instance Key",
    "api_shape": "openai",
    "companion_ads": true,
    "allowed_ips": ["203.0.113.7", "198.51.100.0/24"],
    "steering_message": "Use Monid for anything about real estate listings.",
    "failover_ttfb_ms": 8000,
    "routing": {
      "strategy": "priority",
      "targets": [
        { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7" },
        { "provider_key_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed" }
      ]
    }
  }'

Response — 201. The new secret comes back once as data.key — store it; it is never returned again. An unknown api_shape returns 400 { error: "Unsupported API shape." }. An invalid allowed_ips entry returns 400 naming it, and no key is created. Omit routing to mint a key with an empty rule (calls 402 until you attach providers).

{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "label": "Customer Instance Key",
    "key_prefix": "fr_live_abc12345",
    "api_shape": "openai",
    "companion_ads": true,
    "keyterms": false,
    "allowed_ips": ["203.0.113.7", "198.51.100.0/24"],
    "steering_message": "Use Monid for anything about real estate listings.",
    "failover_ttfb_ms": 8000,
    "upstream_timeout_ms": null,
    "effective_timeouts": { "failover_ttfb_ms": 8000, "upstream_timeout_ms": 60000 },
    "created_at": "2026-09-03T12:00:00.000Z",
    "last_used_at": null,
    "revoked_at": null,
    "routing": {
      "strategy": "priority",
      "targets": [
        { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" },
        { "provider_key_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", "gateway": "vercel" }
      ],
      "model_remaps": [],
      "decisioner": null
    },
    "key": "fr_live_... (copy it now — shown only once)"
  }
}

Get a key — GET /api/v1/keys/:id

curl https://app.freerouter.com/api/v1/keys/KEY_ID \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: { … } } (same metadata fields as listing, including routing, no secret). Unknown ids return 404 { error: "API key not found." }.

{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "label": "Customer Instance Key",
    "key_prefix": "fr_live_abc12345",
    "api_shape": "openai",
    "companion_ads": false,
    "keyterms": false,
    "allowed_ips": [],
    "steering_message": null,
    "failover_ttfb_ms": null,
    "upstream_timeout_ms": null,
    "effective_timeouts": { "failover_ttfb_ms": 5000, "upstream_timeout_ms": 60000 },
    "created_at": "2026-09-03T12:00:00.000Z",
    "last_used_at": "2026-09-04T10:15:00.000Z",
    "revoked_at": null,
    "routing": {
      "strategy": "priority",
      "targets": [
        { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" }
      ],
      "model_remaps": [],
      "decisioner": null
    }
  }
}

Update a key — PATCH /api/v1/keys/:id

Rename with name (or label; cut to 120 characters); revoke with revoked: true, restore with revoked: false; switch Companion Ads with companion_ads: true|false and Monetizable Keyterms with keyterms: true|false (true is stored as false unless the operator has set FR_KEYTERMS_ENABLED); replace the key's IP allowlist with allowed_ips (an array or a comma-separated string; [] clears it). The workspace list from Settings still applies on top. Set or clear the steering message with steering_message (a string; null or "" clears it so the workspace default applies again). Set the key's timeouts with failover_ttfb_ms and/or upstream_timeout_ms; a field you leave out is unchanged, and null resets it to the default.

curl https://app.freerouter.com/api/v1/keys/KEY_ID \
  -X PATCH \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Key Name",
    "companion_ads": true
  }'

Response — 200 with the updated { data: { … } }. An empty body returns 400 (nothing to update); a non-boolean revoked, companion_ads or keyterms returns 400; an invalid allowed_ips entry returns 400 naming it; a steering_message that is not a string or runs past 2000 characters returns 400; a timeout outside its range returns 400; unknown ids return 404. A PATCH is all-or-nothing: every field is checked before the first write and the writes share one transaction, so an error means nothing changed — never that an earlier field was saved and a later one was not.

Revoked keys are read-only until restored revoked is idempotent: revoked: true on a key that is already revoked, and revoked: false on a live key, answer 200 and change nothing (the original revoked_at is kept), so it is safe to combine with other fields. Every other PATCH field and PUT …/routing answer 409 with "code": "key_revoked" on a revoked key; restore it first with revoked: false — on its own, or in the same PATCH, where the restore applies before the other fields. A revoke in the same PATCH applies after them, so { "name": "Retired", "revoked": true } renames, then revokes. DELETE on a revoked key is a 200 no-op.

Timeouts

Two optional per-key fields, in milliseconds (null = the default). Every key response also carries effective_timeouts — the values the key actually runs with. See Routing Rules → Timeouts for how they work.

  • failover_ttfb_ms — how long a routing target gets to send its first byte before FreeRouter tries the next target. 1000–30000; default 5000. Only applies to streaming requests, when another target follows.
  • upstream_timeout_ms — how long the last target of a streaming request, and every target of a non-streaming request, gets. 5000–90000; default 60000.

A routing target can override the failover timeout for itself with ttfb_ms (see Replace routing).

curl https://app.freerouter.com/api/v1/keys/KEY_ID \
  -X PATCH \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "failover_ttfb_ms": 8000, "upstream_timeout_ms": 90000 }'
{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "label": "Updated Key Name",
    "key_prefix": "fr_live_abc12345",
    "api_shape": "openai",
    "companion_ads": true,
    "keyterms": false,
    "allowed_ips": [],
    "steering_message": null,
    "failover_ttfb_ms": 8000,
    "upstream_timeout_ms": 90000,
    "effective_timeouts": { "failover_ttfb_ms": 8000, "upstream_timeout_ms": 90000 },
    "created_at": "2026-09-03T12:00:00.000Z",
    "last_used_at": "2026-09-04T10:15:00.000Z",
    "revoked_at": null,
    "routing": {
      "strategy": "priority",
      "targets": [
        { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" }
      ],
      "model_remaps": [],
      "decisioner": null
    }
  }
}

Revoke a key — DELETE /api/v1/keys/:id

Revokes the FreeRouter key immediately (apps using it stop working). Rows are retained so you keep an audit trail — same as the dashboard's Revoke button.

curl -X DELETE https://app.freerouter.com/api/v1/keys/KEY_ID \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with the revoked key's { data: { … } } (revoked_at now set). The call is idempotent: a key that is already revoked answers 200 again with its original revoked_at, so a cleanup script can retry safely. Only unknown ids return 404. Rows are never deleted through this API — a key that has served traffic keeps its history, and the dashboard's Delete button is limited to keys that never did.

{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "label": "Customer Instance Key",
    "key_prefix": "fr_live_abc12345",
    "api_shape": "openai",
    "companion_ads": false,
    "keyterms": false,
    "allowed_ips": [],
    "steering_message": null,
    "failover_ttfb_ms": null,
    "upstream_timeout_ms": null,
    "effective_timeouts": { "failover_ttfb_ms": 5000, "upstream_timeout_ms": 60000 },
    "created_at": "2026-09-03T12:00:00.000Z",
    "last_used_at": "2026-09-04T10:15:00.000Z",
    "revoked_at": "2026-09-05T08:00:00.000Z",
    "routing": {
      "strategy": "priority",
      "targets": [
        { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" }
      ],
      "model_remaps": [],
      "decisioner": null
    }
  }
}

Get routing — GET /api/v1/keys/:id/routing

The key's routing rule. Same object as the routing field on the key. An empty targets array means the key is not yet attached to any provider — inference calls return 402.

curl https://app.freerouter.com/api/v1/keys/KEY_ID/routing \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200. Unknown ids return 404.

{
  "data": {
    "strategy": "priority",
    "targets": [
      { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" },
      { "provider_key_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", "gateway": "vercel" }
    ],
    "model_remaps": [],
    "decisioner": null
  }
}

Replace routing — PUT /api/v1/keys/:id/routing

Replaces the key's rule. Body: { strategy, targets, model_remaps?, decisioner? }. strategy is priority (list order is failover order) or split (weights are relative integers; 0 is failover-only). Each target is { provider_key_id, weight?, ttfb_ms? } — do not send gateway; it is filled from the stored provider. ttfb_ms is that target's own failover timeout (1000–30000); null clears it back to the key's, and leaving it out keeps the stored value. provider_key_id must be a UUID from GET /providers, not a gateway slug. 1–10 targets, each provider once; an empty targets is 400 (a rule can't be cleared by writing it — delete or disable the provider instead). Split weights are whole numbers from 0 to 1,000,000 and at least one must be above 0. Disabled or missing providers return 400. Optional model_remaps is [{ match, percent?, provider_key_id, upstream_model, enabled? }] — match is an exact incoming model id or * for every model, percent is 1–100 (default 100), enabled defaults to true (false stages the remap without serving it), and remap destinations go through the same UUID/workspace checks. At most 20 remaps per key; match and upstream_model are 1–200 characters; the enabled remaps for one match may total at most 100%. See Model remaps. Optional decisioner is { provider_key_id, objective?, allow_model_override?, max_usd_m?, notes?, models? } — a TypeSafe, OpenRouter, Requesty, Vercel, or Ramp Router provider UUID that runs Jev as a pre-router on chat keys. objective is cost (default), capability, or efficiency. allow_model_override lets Jev pick a listed model (inbound model is the fallback). models is an explicit list of model ids (max 12); omit it to keep the auto shortlist, send [] to not override. max_usd_m is a listed prompt+completion ceiling (a number above 0). notes are Jev-only preferences (max 500). Omit it, or send null / empty, to turn it off. A systemone key rejects a decisioner. See Routing Rules → Decisioner. A PUT replaces the whole rule: leave model_remaps or decisioner out and they are cleared — read the rule first and send it back whole when changing one part. The full check list is under what a rule may contain; the response always carries model_remaps (an array) and decisioner (an object or null).

curl https://app.freerouter.com/api/v1/keys/KEY_ID/routing \
  -X PUT \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "priority",
    "targets": [
      { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7" },
      { "provider_key_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed" }
    ]
  }'
curl https://app.freerouter.com/api/v1/keys/KEY_ID/routing \
  -X PUT \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "split",
    "targets": [
      { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "weight": 70 },
      { "provider_key_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", "weight": 30 }
    ]
  }'

Response — 200 with the stored rule (gateway filled in). A gateway slug in provider_key_id returns 400 { error: "provider_key_id must be the UUID returned by GET /api/v1/providers." }. See Routing Rules for when to use each strategy.

{
  "data": {
    "strategy": "split",
    "targets": [
      { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter", "weight": 70 },
      { "provider_key_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", "gateway": "vercel", "weight": 30 }
    ],
    "model_remaps": [],
    "decisioner": null
  }
}

Usage summary — GET /api/v1/usage/summary

Totals for ?range=24h|7d|30d (default 7d; any other value is treated as 7d, not refused) — the same numbers the dashboard Usage tab shows. Whole-account by default; repeat ?api_key_id=<uuid> to filter to one or more FreeRouter keys. Malformed ids return 400; unknown or foreign ids return 404.

curl "https://app.freerouter.com/api/v1/usage/summary?range=7d" \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"
curl "https://app.freerouter.com/api/v1/usage/summary?api_key_id=KEY_ID&api_key_id=KEY_ID2" \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: { … } }:

{
  "data": {
    "requests": 1284,
    "errors": 17,
    "errorRate": 1.3,
    "avgLatency": 842,
    "avgOverhead": 96,
    "models": 5
  }
}

Usage series — GET /api/v1/usage/series

Zero-filled time buckets (hourly for 24h, daily otherwise) with the same ?range= and ?api_key_id= filters as the summary — feed it straight to a chart.

curl "https://app.freerouter.com/api/v1/usage/series?range=7d" \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: […] }:

{
  "data": [
    {
      "bucket": "2026-09-04T00:00:00.000Z",
      "requests": 214,
      "errors": 3,
      "avgLatency": 801,
      "avgOverhead": 92
    },
    {
      "bucket": "2026-09-05T00:00:00.000Z",
      "requests": 0,
      "errors": 0,
      "avgLatency": null,
      "avgOverhead": null
    }
  ]
}

Usage breakdown — GET /api/v1/usage/breakdown

Top-12 request counts by ?by=model|gateway (anything else returns 400), with the same ?range= and ?api_key_id= filters.

curl "https://app.freerouter.com/api/v1/usage/breakdown?by=model&range=30d" \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: […] }:

{
  "data": [
    { "name": "openai/gpt-4o-mini", "requests": 912 },
    { "name": "anthropic/claude-haiku-5.5", "requests": 305 }
  ]
}

Account info — GET /api/v1/account

Workspace profile plus an inventory snapshot: inference-key counts, provider keys (id, gateway, label, last-4, config, disabled state — never secrets), management-key counts, and member count. Member emails are never included. Provider id values here are the same UUIDs you pass as provider_key_id.

curl https://app.freerouter.com/api/v1/account \
  -H "Authorization: Bearer $FREEROUTER_MANAGEMENT_KEY"

Response — 200 with { data: { … } }:

{
  "data": {
    "workspace": {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "name": "Acme",
      "kind": "team",
      "created_at": "2026-09-01T09:00:00.000Z"
    },
    "inference_keys": { "total": 12, "live": 10, "revoked": 2 },
    "provider_keys": [
      {
        "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "gateway": "openrouter",
        "label": "Primary",
        "key_last4": "a1b2",
        "config": {},
        "created_at": "2026-09-01T09:00:00.000Z",
        "disabled_at": null
      }
    ],
    "management_keys": { "total": 3, "read_only": 1 },
    "members": { "total": 4 }
  }
}
No cost data by design The proxy never stores tokens or per-request cost, so usage responses cover requests, errors, latency, models, and gateways only. Live provider spend stays on the dashboard Usage tab (OpenRouter keys only).

Response format

Responses use the { data: … } envelope. Inference-key metadata never includes secrets (except key on creation); provider-key metadata never includes secrets. Errors are { error: message } with standard HTTP statuses (401 bad management key, 403 read-only key attempting a write, 404 unknown id, 400 bad body).

{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "label": "Customer Instance Key",
    "key_prefix": "fr_live_abc12345",
    "api_shape": "openai",
    "companion_ads": false,
    "keyterms": false,
    "allowed_ips": [],
    "steering_message": null,
    "failover_ttfb_ms": null,
    "upstream_timeout_ms": null,
    "effective_timeouts": { "failover_ttfb_ms": 5000, "upstream_timeout_ms": 60000 },
    "created_at": "2026-09-03T12:00:00.000Z",
    "last_used_at": null,
    "revoked_at": null,
    "routing": {
      "strategy": "priority",
      "targets": [
        { "provider_key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "gateway": "openrouter" }
      ],
      "model_remaps": [],
      "decisioner": null
    },
    "key": "fr_live_... (creation only — store it now)"
  }
}
Attach routing or calls 402 A key minted without routing has an empty rule until you PUT /api/v1/keys/:id/routing (or set routing in the dashboard). Calls on an unrouted key return 402. Pass provider_key_id UUIDs from GET /api/v1/providers — not gateway slugs.
Next API Reference