Management API v8
Base path: http://localhost:8317/v8/management
This is the configuration and operations API for the v8 layout. Configuration paths mirror the v8 YAML tree documented in Configuration Options. A successful configuration write persists the file and hot-reloads the service.
/v0/management remains available for existing clients, but it is near deprecation. Do not build new functionality against that API.
Authentication
- Every request except the OAuth callback must carry a valid management key, including requests from localhost.
- Remote access requires
management.allow-remote: true. Until a v8 write migrates the file, the legacy nameremote-management.allow-remotestill works. - Send the plaintext key with either header:
Authorization: Bearer <plaintext-key>X-Management-Key: <plaintext-key>
Additional notes:
MANAGEMENT_PASSWORDregisters an extra in-memory management secret and keeps remote management enabled even whenallow-remoteis false. It is never written to disk.cliproxy run --password <pwd>and the SDKWithLocalManagementPasswordaccept that password from localhost (127.0.0.1or::1) only. It stays in memory.- Routes return 404 when
management.secret-keyis empty,MANAGEMENT_PASSWORDis unset, and no local management password was configured. The same 404 applies to/v0/management. - Home mode does not expose this API and also returns 404.
- Five consecutive authentication failures from one client IP, including localhost, impose a temporary ban of about 30 minutes.
- A plaintext
management.secret-keyis bcrypt-hashed when the configuration is loaded or saved.
Request and response conventions
- Authenticated configuration and operation bodies use
Content-Type: application/jsonunless the endpoint says otherwise. - A v8 configuration body is the value itself. Do not wrap it in
{ "value": ... },{ "items": ... }, or a legacy field name. GET /configandGET /config/<path>return that YAML node as JSON. A missing path returns404with{ "error": "not_found" }.PUTreplaces the selected node.PATCHdeep-merges objects and replaces every other kind.nullis stored; it does not delete a field. UseDELETEto remove a field.- Path segments are YAML mapping keys. They are not array indexes. Lists are replaced as a whole.
- A successful configuration mutation returns
{ "status": "ok", "config-version": 8 }and hot-reloads the saved file. GETreturns a v8 view but does not rewrite the file. The first successfulPUT,PATCH, orDELETEmigrates a legacy file toconfig-version: 8, removes legacy spellings, and preserves comments. A rejected write does not migrate the file.- v8 writes reject legacy field names and unknown root sections. See the legacy map in Configuration Options.
These fields are owned by Home. Changing them returns 400 with { "error": "read_only_field", "field": "<path>" }:
credentials/concurrency/lifecycle-config-revisioncredentials/concurrency/observation-barrier-revisionplugins/auth-revision
Configuration
Field names, defaults, and provider rules are defined in Configuration Options. The examples below show only the transport.
Read configuration
GET /config— full v8 document.GET /config/<section>/<key>/...— one nested node.GET /config.yaml— the same v8 view as YAML.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/configcurl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/config/routing/strategy"round-robin"curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Accept: application/yaml' \
http://localhost:8317/v8/management/config.yamlNotes:
- Responses send
Cache-Control: no-store. - YAML uses
Content-Type: application/yaml; charset=utf-8. - JSON reads omit
usernameandcredentialfromoauth.providers.codex.live-media-relay.ice-servers. The YAML read still contains them. - When no usable configuration can be read, the handler returns
500with{ "error": "read_failed" }or{ "error": "invalid_config" }.
Replace or merge configuration
PUT /configreplaces the whole document. The body must be a JSON object.PATCH /configdeep-merges a JSON object into the document.PUT /config/<path>replaces that node. The body is the raw JSON value: object, array, string, number, boolean, ornull.PATCH /config/<path>merges when both the current node and the body are objects; otherwise it replaces the node.PUT /config.yamlreplaces the file from a v8 YAML document.Content-Typemay beapplication/yaml. There is noPATCHfor/config.yaml.
curl -X PATCH -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '{"routing":{"retry":{"request-retry":0}},"oauth":{"providers":{"aistudio":{"ws-auth":false}}}}' \
http://localhost:8317/v8/management/configcurl -X PUT -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '"direct"' \
http://localhost:8317/v8/management/config/requests/proxy-urlcurl -X PUT -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '["client-key-1","client-key-2"]' \
http://localhost:8317/v8/management/config/access/api-keyscurl -X PATCH -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '{"enabled":true,"priority":10}' \
http://localhost:8317/v8/management/config/plugins/configs/example-plugincurl -X PUT -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/yaml' \
--data-binary @config.yaml \
http://localhost:8317/v8/management/config.yamlResponse:
{ "status": "ok", "config-version": 8 }Upstream API keys are lists of groups. Replace the provider list; a path cannot select api-keys/codex/0.
curl -X PUT -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '[{"name":"codex-1","base-url":"https://example.invalid","keys":[{"api-key":"sk-example","weight":1}]}]' \
http://localhost:8317/v8/management/config/api-keys/codexNotes:
- A legacy envelope such as
{ "value": 0 }is not a v8 value and fails validation. - Unknown sections, legacy names, and values that fail configuration parsing are rejected. The previous file stays in place.
- Writing the redacted JSON ICE-server list back preserves TURN
usernameandcredentialfor an entry with the sameurls. An explicit empty string ornullclears the secret. A YAML replacement does not preserve omitted secrets. - The write updates the existing config file in place, so a Docker file mount keeps the same inode.
- Changing
management.secret-keyormanagement.allow-remoteis possible. An empty secret with no fallback password makes later management calls return 404.
Delete a configuration field
DELETE /config/<path>removes that field and prunes mapping ancestors that become empty.DELETE /configis rejected.
curl -X DELETE -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/config/requests/proxy-urlServer
Latest version
GET /server/latest-version— latest GitHub release tag. It does not download assets.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/server/latest-version{ "latest-version": "v1.2.3" }The lookup uses https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest with User-Agent: CLIProxyAPI. A configured requests.proxy-url is honored.
Requests
Authenticated upstream call
POST /requests/api-call— send one outbound HTTP request, optionally with a stored credential.
curl -X POST -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '{"auth_index":"a1b2","method":"GET","url":"https://api.example.com/v1/ping","header":{"Authorization":"Bearer $TOKEN$"}}' \
http://localhost:8317/v8/management/requests/api-call{ "status_code": 200, "header": { "Content-Type": ["application/json"] }, "body": "{\"ok\":true}" }Notes:
- Required fields are
methodand an absoluteurl. Optional fields are string-mapheader, raw-stringdata, andproxy_url. auth_indexis also accepted asauthIndexorAuthIndex.$TOKEN$in a header is replaced with the selected credential's access token or API key.- The credential proxy takes precedence over the request
proxy_urland the global proxy. The response preserves the upstream status instatus_code. - This can call arbitrary URLs with stored credentials. Protect the management key.
Routing
Reset cooldown
POST /routing/cooldown/reset— clear quota and cooldown state for one credential.
curl -X POST -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '{"auth_index":"a1b2"}' \
http://localhost:8317/v8/management/routing/cooldown/reset{ "status": "ok", "auth_index": "a1b2", "models": ["gpt-5.4"] }An unknown auth_index returns 404 with { "error": "auth not found" }.
Static model definitions
GET /routing/model-definitions/:channel— static catalog metadata for one channel.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/routing/model-definitions/codexThe response is { "channel": "codex", "models": [ ... ] }.
An unknown channel returns 400 with { "error": "unknown channel", "channel": "..." }.
Observability
Application logs
GET /observability/logs— read log lines.DELETE /observability/logs— delete rotated logs and truncate the active log.
Query parameters for GET:
after: Unix timestamp. Return only newer lines.limit: maximum lines. Withlimitand noafter, the newest lines are returned.cursor: opaque cursor from a previousnext-cursor.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
'http://localhost:8317/v8/management/observability/logs?limit=2'{
"lines": ["2026-05-05 12:00:00 info request accepted"],
"line-count": 1,
"latest-timestamp": 1777982400,
"next-cursor": "<OPAQUE_CURSOR>"
}Notes:
- File logging must be enabled through
observability.logs.logging-to-file. Otherwise the response is400with{ "error": "logging to file disabled" }. - A missing log file returns empty
linesandline-count: 0. - Pass
next-cursorback ascursor. A reset response includes"cursor-reset": true. DELETEreturns{ "success": true, "message": "Logs cleared successfully", "removed": 3 }.
Request error logs
GET /observability/logs/errors— listerror-*.logfiles.GET /observability/logs/errors/:name— download one error log.GET /observability/logs/requests/:id— download the request log whose filename ends in-<id>.log.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/observability/logs/errors{ "files": [{ "name": "error-2026-05-05.log", "size": 12345, "modified": 1777982400 }] }Notes:
- When request logging is enabled, the error-log list is empty.
:namemust be an existingerror-*.logfilename without path separators.:idmust not contain path separators.
Usage
GET /observability/usage/queue?count=10— pop up tocountusage records.countdefaults to1and must be a positive integer. Records are removed from the in-memory queue. An empty queue returns[].GET /observability/usage/api-keys— in-memory success and failure buckets for API-key credentials, grouped by provider and keyed bybase_url|api_key.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
'http://localhost:8317/v8/management/observability/usage/queue?count=10'The local Redis RESP usage output is disabled. Enable aggregation with observability.usage.usage-statistics-enabled before expecting records.
Credentials
These routes manage files and runtime state under oauth.auth-dir. They do not edit api-keys groups; use the configuration routes for those.
List, upload, and delete
GET /credentials— list credential files and runtime records.POST /credentials— upload one.jsoncredential, as multipart fieldfileor as a raw JSON body with?name=<file.json>.DELETE /credentials?name=<file.json>— delete one on-disk credential and disable it in the runtime.DELETE /credentials?all=true— delete every on-disk.jsoncredential. The response is{ "status": "ok", "deleted": 3 }.GET /credentials/download?name=<file.json>— download one on-disk credential.GET /credentials/models?name=<file-or-id>— model definitions for one credential:{ "models": [ ... ] }.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
http://localhost:8317/v8/management/credentials{
"files": [
{
"id": "[email protected]",
"auth_index": "a1b2c3d4e5f67890",
"name": "[email protected]",
"provider": "claude",
"status": "ready",
"disabled": false,
"unavailable": false,
"runtime_only": false,
"source": "file"
}
]
}Notes:
- Entries are sorted by
name.runtime_only: truemeans the credential exists only in memory; those entries cannot be downloaded or deleted here. - Upload requires the core auth manager. If it is unavailable the response is
503with{ "error": "core auth manager unavailable" }. - Upload filenames must end in
.json. A successful upload is registered immediately and returns{ "status": "ok" }.
Status, fields, and refresh
PATCH /credentials/status—{ "name": "<file-or-id>", "disabled": true }. API-key records are disabled through their excluded-model configuration. A plugin virtual child cannot be changed on its own.PATCH /credentials/fields—{ "name": "<file-or-id>", ...fields }. Dot paths update nested metadata, for exampleheaders.X-Team. Aheadersobject merges with existing headers; an empty value removes that header.POST /credentials/refresh— refresh file-backed OAuth credentials.
curl -X PATCH -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '{"name":"codex-user.json","disabled":true}' \
http://localhost:8317/v8/management/credentials/statusOAuth
Start a login
GET /oauth/auth-url?provider=<provider>— start a provider login and return the browser URL.
Providers: claude, codex, antigravity, kimi, kimi-ai, xai, devin, meta, and an OAuth provider registered by a plugin.
curl -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
'http://localhost:8317/v8/management/oauth/auth-url?provider=claude&is_webui=true'{ "status": "ok", "url": "https://...", "state": "anth-1716206400" }Notes:
- A missing
providerreturns400with{ "error": "provider is required" }. An unknown provider returns404with{ "error": "provider_not_found" }unless a plugin handles it. is_webui=truereuses the management UI callback forwarder for providers that support it.- Device-code providers can also return
flow,user_code, andexpires_in.
Poll and cancel
GET /oauth/status?state=<state>—waitwhile pending,okafter success, orerrorwith anerrorstring. Completed states remain briefly so a client can observeok.DELETE /oauth/session?state=<state>— cancel a pending session. Response:{ "status": "ok", "cancelled": true }. A cancelled flow does not store credentials.
Import
POST /oauth/import?provider=vertex— import a Google service-account JSON file.vertexis the supported provider.
curl -X POST -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-F 'file=@/path/to/service-account.json' \
-F 'location=us-central1' \
'http://localhost:8317/v8/management/oauth/import?provider=vertex'{
"status": "ok",
"auth-file": "/abs/path/auths/vertex-my-project.json",
"project_id": "my-project",
"email": "[email protected]",
"location": "us-central1"
}The upload is multipart/form-data in the file field. location is optional and defaults to us-central1.
Callback
GET /oauth/callback and POST /oauth/callback are outside the management-key middleware. They accept a callback only for a pending state whose provider matches the session.
GETreadsprovider,state,code, anderrororerror_description.POSTaccepts{ "provider", "redirect_url", "code", "state", "error" }.redirect_urlmay carry the callback query.
curl 'http://localhost:8317/v8/management/oauth/callback?provider=codex&state=codex-...&code=AUTHORIZATION_CODE'{ "status": "ok" }Plugins
Plugin enablement and plugin-owned settings are configuration, not separate routes:
GET /config/pluginsreads the plugin section.PUTorPATCH /config/plugins/configs/<plugin-id>replaces or merges one plugin object.PUT /config/plugins/configs/<plugin-id>/enabledwithtrueorfalsechanges only that flag. It does not changeplugins.enabled.
Discovery and store
GET /plugins— discovered, configured, and registered plugins, includingplugins_enabled,plugins_dir, and per-plugin id, path, enabled state, metadata, config fields, and menus.DELETE /plugins/:id— remove the local plugin file and its saved configuration. A plugin that cannot be unloaded returns409and may setrestart_required: true.GET /plugins/store— store catalog, source errors, install state, and update availability.POST /plugins/store/:id/install— download or update one plugin and enable it. Use?source=<source-id>when IDs collide.versionmay be a query parameter or{ "version": "1.2.3" }.
curl -X POST -H 'Authorization: Bearer <MANAGEMENT_KEY>' \
-H 'Content-Type: application/json' \
-d '{"version":"1.2.3"}' \
'http://localhost:8317/v8/management/plugins/store/example-plugin/install?source=official'{
"status": "installed",
"source_id": "official",
"id": "example-plugin",
"version": "1.2.3",
"path": "/abs/path/plugins/example-plugin.so",
"restart_required": false
}Store installs can download executable artifacts. Trust store sources before enabling them.
Plugin quota
GET /plugins/:id/quota?auth_index=<auth-index>POST /plugins/:id/quotawith{ "auth_index": "<auth-index>" }DELETE /plugins/:id/quota?auth_index=<auth-index>
authIndex is accepted as an alias. A missing quota provider returns 404. Provider failures return 502. v8 does not register the legacy /quota/reset alias.
Error responses
Configuration and operation handlers use their own error strings. Common results are:
400{ "error": "invalid_json" },{ "error": "invalid_body" },{ "error": "invalid_path" },{ "error": "config_must_be_object" },{ "error": "cannot_delete_config" }, or{ "error": "invalid_config", "message": "..." }400{ "error": "read_only_field", "field": "plugins/auth-revision" }401{ "error": "missing management key" }or{ "error": "invalid management key" }403{ "error": "remote management disabled" }404{ "error": "not_found" },{ "error": "provider_not_found" }, or{ "error": "auth not found" }409plugin unload or restart required422{ "error": "invalid_config", "message": "..." }when the document parses but fails configuration validation500{ "error": "write_failed", "message": "..." }or{ "error": "read_failed" }502quota provider failure503{ "error": "core auth manager unavailable" }
An empty management secret with no fallback password returns 404 before these handlers run.
Notes
- New clients should send v8 paths only. After the file is migrated, a legacy
/v0/managementupdate still edits the effective value, but it is not a contract for new work. quota-exceeded.switch-projectandquota-exceeded.switch-preview-modelhave no v8 names. Do not add features that depend on them.- OAuth provider settings under
oauth.providersdo not apply toapi-keysgroups. Keep those credentials in the group and key fields documented for upstream API keys.