The Mihomo core ships a RESTful API. Clash Verge's own interface talks to the core through it — understand the endpoints and you can script anything the interface can do.
Enabling it, and the security implications
external-controller: 127.0.0.1:9090
secret: "a sufficiently long random string"Generate a random secret:
openssl rand -hex 24Authentication
Every request carries a bearer token:
curl -H "Authorization: Bearer your-secret" http://127.0.0.1:9090/versionWebSocket endpoints (logs, traffic) accept a query parameter instead:
ws://127.0.0.1:9090/traffic?token=your-secretThe full endpoint table
| Method | Path | Purpose |
|---|---|---|
| GET | /version | Core version |
| GET | /configs | Current configuration |
| PATCH | /configs | Change runtime settings (ports, mode and so on) |
| PUT | /configs?force=true | Reload the config file |
| GET | /proxies | Every node and policy group |
| GET | /proxies/:name | Detail for one node or group |
| PUT | /proxies/:name | Change the selected node in a group |
| GET | /proxies/:name/delay | Latency-test one node |
| GET | /group/:name/delay | Test a whole policy group |
| GET | /connections | All current connections |
| DELETE | /connections | Close every connection |
| DELETE | /connections/:id | Close one connection |
| GET | /rules | The rule list currently in effect |
| GET | /providers/proxies | All proxy-providers |
| PUT | /providers/proxies/:name | Update one provider by hand |
| GET | /providers/rules | All rule-providers |
| PUT | /providers/rules/:name | Update a rule set by hand |
| GET | /logs | Log stream (WebSocket) |
| GET | /traffic | Live traffic (WebSocket) |
| GET | /memory | Memory use (WebSocket) |
Common operations
List every policy group and its current selection
curl -s -H "Authorization: Bearer $SECRET" \
http://127.0.0.1:9090/proxies | jq '.proxies | to_entries[] | select(.value.type=="Selector") | {group: .key, now: .value.now}'Switch a node
curl -X PUT \
-H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"HK-01"}' \
http://127.0.0.1:9090/proxies/PROXYGroup names containing spaces or emoji need URL encoding:
GROUP=$(printf '%s' "🚀 Select" | jq -sRr @uri)
curl -X PUT -H "Authorization: Bearer $SECRET" \
-d '{"name":"HK-01"}' \
"http://127.0.0.1:9090/proxies/$GROUP"Latency-test a node
curl -s -H "Authorization: Bearer $SECRET" \
"http://127.0.0.1:9090/proxies/HK-01/delay?timeout=5000&url=http%3A%2F%2Fwww.gstatic.com%2Fgenerate_204"
# returns {"delay":123}Change the proxy mode
curl -X PATCH -H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{"mode":"global"}' \
http://127.0.0.1:9090/configsmode accepts rule, global or direct. The same endpoint also changes log-level, allow-lan and others.
Reload the configuration
curl -X PUT -H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{"path":"/etc/mihomo/config.yaml"}' \
"http://127.0.0.1:9090/configs?force=true"Close all connections
curl -X DELETE -H "Authorization: Bearer $SECRET" \
http://127.0.0.1:9090/connectionsSwitching nodes does not move existing connections (an established TCP connection keeps using the old node); this forces everything to reconnect.
Update a provider
# refresh the node subscription
curl -X PUT -H "Authorization: Bearer $SECRET" \
http://127.0.0.1:9090/providers/proxies/main
# refresh a rule set
curl -X PUT -H "Authorization: Bearer $SECRET" \
http://127.0.0.1:9090/providers/rules/cn-domainUseful scripts
1. Switch to the lowest-latency node automatically
#!/bin/bash
# pick-fastest.sh — test, then switch to the fastest node
set -euo pipefail
API="http://127.0.0.1:9090"
SECRET="your-secret"
GROUP="AUTO"
TEST_URL="http%3A%2F%2Fwww.gstatic.com%2Fgenerate_204"
# trigger a test across the group
curl -s -H "Authorization: Bearer $SECRET" \
"$API/group/$GROUP/delay?timeout=5000&url=$TEST_URL" > /dev/null
# find the lowest latency
BEST=$(curl -s -H "Authorization: Bearer $SECRET" "$API/proxies" \
| jq -r --arg g "$GROUP" '
.proxies[$g].all[] as $n
| .proxies[$n]
| select(.history | length > 0)
| select(.history[-1].delay > 0)
| "\(.history[-1].delay) \(.name)"
' | sort -n | head -1 | cut -d' ' -f2-)
echo "fastest node: $BEST"
curl -s -X PUT -H "Authorization: Bearer $SECRET" \
-d "{\"name\":\"$BEST\"}" "$API/proxies/$GROUP" > /dev/null2. Watch node health and alert when everything is down
#!/bin/bash
# health-watch.sh
API="http://127.0.0.1:9090"
SECRET="your-secret"
ALIVE=$(curl -s -H "Authorization: Bearer $SECRET" "$API/proxies" \
| jq '[.proxies[] | select(.type != "Selector" and .type != "URLTest")
| select(.history | length > 0)
| select(.history[-1].delay > 0)] | length')
if [ "$ALIVE" -eq 0 ]; then
echo "$(date '+%F %T') no usable nodes" >> /var/log/mihomo-alert.log
# hook your notification channel in here
fi3. List the heaviest connections
curl -s -H "Authorization: Bearer $SECRET" http://127.0.0.1:9090/connections \
| jq -r '.connections
| sort_by(-.download)
| .[:10][]
| "\(.download/1048576 | floor)MB \(.metadata.host // .metadata.destinationIP) \(.metadata.processPath // "-")"'Very handy for tracking down "what is eating my data".
4. Follow the log live
# needs websocat or wscat
websocat "ws://127.0.0.1:9090/logs?token=your-secret&level=info"Deploying a web dashboard
A dashboard is a purely static page that talks to the core through the API.
Option A: served by the core (recommended)
external-ui: /etc/mihomo/ui
external-ui-name: metacubexd
external-ui-url: "https://dashboard-release-address/dist.zip"Extract the dashboard files into the directory named by external-ui, then visit:
http://core-address:9090/uiOn first open you supply the API address (http://core-address:9090) and the secret.
Option B: use a publicly hosted dashboard
A dashboard is pure frontend, so you can also use someone else's hosted page and point it at your API address and secret.
Comparing common dashboards
A few clever uses of the API
Scheduled node switching
Move to a dedicated line at peak hours and back to an ordinary node during the day:
# crontab
0 20 * * * /usr/local/bin/switch-node.sh "IPLC-HK"
0 1 * * * /usr/local/bin/switch-node.sh "AUTO"A keyboard shortcut (desktop)
Paired with AutoHotkey on Windows or Hammerspoon on macOS, one shortcut toggles between global and rule mode:
# switch to global
curl -X PATCH -H "Authorization: Bearer $SECRET" \
-d '{"mode":"global"}' http://127.0.0.1:9090/configsFeeding a monitoring system
/traffic is a WebSocket endpoint pushing upload and download rates every second. Collect it into a time-series database and you have traffic graphs.
Diagnostics
A quick check:
curl -i -H "Authorization: Bearer $SECRET" http://127.0.0.1:9090/versionA 200 with a version JSON means you are through.
In short
- The API can do everything the interface can, including switching nodes, changing mode and reloading the config
secretis required, especially when listening on0.0.0.0- Group names with spaces or emoji need URL encoding
- After switching nodes, remember
DELETE /connections, or existing connections keep using the old node - A dashboard is only a frontend; self-hosting is safer than a third-party page
Related: Mihomo as a home gateway and diagnosing traffic and connections.
Related docs
127.0.0.1 inside a container is not the host. Configuration for all three Docker scenarios — daemon pulls, build time and run time — plus two approaches for WSL2's different networking modes.
A reference table worth bookmarking. The proxy command for each tool, where its config file lives, how to undo it, and why some tools ignore your environment variables.
Zero configuration for every device in the house. The trade-offs between three approaches (side router, TProxy, TUN), the full config and systemd service, firewall rules, and fault tolerance so the household stays online.