Clash External Controller API: Advanced Node Switching Guide
In the world of proxy tools, Clash is only as powerful as the subscription you feed it. Picking a high-quality "Airport" (proxy provider) is the difference between a seamless 4K streaming experience and a frustrating connection that drops every five minutes. This guide breaks down the technical metrics, price traps, and security protocols you must know in 2026.
Why Use the Clash External Controller API?
Clash is often introduced as a desktop proxy client with a node picker, but its External Controller API makes it much more useful than a manual GUI suggests. Once the controller is enabled, another application can inspect the running Clash core, read proxy groups, measure node latency, change the active policy member, and react to failures without requiring you to open the client window. That turns Clash into a small, automatable proxy control plane for a workstation, server, lab, or deployment environment.
This capability is especially valuable when a subscription contains dozens of nodes and the best choice changes throughout the day. A node that was fast in the morning may become congested, temporarily unreachable, or unsuitable for a particular destination later. Instead of hard-coding one proxy name into every script, you can ask Clash which nodes are available, test them, and select a healthy member in the relevant policy group. The same approach works for a scheduled maintenance task, a long-running download, a CI runner, or a home server that needs to recover from an unstable connection.
The API is exposed by the Clash-compatible core used by many current clients, including Mihomo, Clash Verge Rev, and other applications built around the Clash configuration model. Endpoint details can differ slightly between versions and forks, so treat the API response from your own client as the source of truth. The core concepts remain consistent: an external controller address, an optional secret, a REST-style HTTP interface, proxy objects, and selectable proxy groups.
Security first
Never expose an unauthenticated external controller on a public IP address. A user who can reach the controller may be able to switch traffic, inspect connection metadata, reload configuration, or change runtime behavior. Bind it to loopback whenever possible and configure a secret before writing automation.
Controller Settings and API Prerequisites
Before sending requests, confirm that the active profile actually enables an external controller. In a YAML configuration, the relevant settings commonly look like this:
external-controller: 127.0.0.1:9090
secret: "replace-with-a-long-random-secret"
Some clients expose these values in a settings panel instead of requiring manual YAML editing. The address is normally written as host:port. Use 127.0.0.1 or localhost for scripts running on the same machine. Binding to 0.0.0.0 allows connections from other interfaces and should only be considered when you have a specific remote-management design, firewall restrictions, and strong authentication.
After saving the setting, restart or reload the Clash profile according to the client’s behavior. Then verify that the port is listening locally. On Windows, PowerShell can help locate the listener; on macOS and Linux, tools such as lsof or ss are useful. A refused connection usually means the controller is disabled, the port is different from the one you expected, or the client has not reloaded the profile.
Prepare these values before testing:
- Controller URL: for example,
http://127.0.0.1:9090 - API secret: the value configured under
secret, if authentication is enabled - Policy group name: such as
Proxy,Auto, or a provider-specific group - Node names: the exact names returned by the API, including spaces, symbols, and regional suffixes
- Delay URL: a reachable HTTPS endpoint used consistently for latency tests
Authentication is usually sent with the Authorization header in Bearer format. Keep the secret out of source control, shell history, and screenshots. A practical pattern is to store it in an environment variable and let the script read that variable at runtime. If your client does not require a secret for loopback access, adding one is still recommended because local malware, another user account, or a mistakenly forwarded port could otherwise control the service.
Do not assume that the API port is the same as the mixed proxy port. The mixed port accepts application traffic, while the external controller accepts management requests. Confusing the two produces errors that look like a broken API even though Clash itself is working normally.
Read Clash Status, Groups, and Current Nodes
Start with read-only requests. They let you learn the actual runtime structure before attempting a node switch. The version endpoint is a simple connectivity test:
curl -H "Authorization: Bearer $CLASH_SECRET" \
http://127.0.0.1:9090/version
A successful response normally contains the core version and a meta or premium indicator. The exact JSON fields may vary, but an HTTP success response confirms that the controller address, port, and authentication header are correct.
Next, request the complete proxy collection:
curl -s \
-H "Authorization: Bearer $CLASH_SECRET" \
http://127.0.0.1:9090/proxies
The response is a JSON object containing proxy entries. A selectable policy group generally has a type such as Select, URLTest, Fallback, or LoadBalance, along with a list of members. A leaf proxy represents an actual node, while a group represents a decision layer that may contain nodes or other groups. The currently active member is commonly exposed through a field such as now.
Do not build a script around the visual order shown in a GUI. Use the API’s returned names and types. Provider names can contain commas, brackets, emoji, slashes, or non-ASCII characters. A script that assumes a node is always at array position three will silently select the wrong destination after a subscription update. Prefer exact name matching, stable provider prefixes, or a configurable allowlist.
A small Python inspection script can make the structure easier to understand:
import os
import requests
base = "http://127.0.0.1:9090"
headers = {"Authorization": f"Bearer {os.environ['CLASH_SECRET']}"}
data = requests.get(f"{base}/proxies", headers=headers, timeout=5).json()
for name, proxy in data.get("proxies", {}).items():
if proxy.get("type") in {"Selector", "URLTest", "Fallback", "LoadBalance"}:
print(name, "=>", proxy.get("now"), proxy.get("all", []))
Some Clash-compatible cores use Selector while others return a related group type or additional fields. The important lesson is to inspect the response rather than enforce one version-specific assumption. You can also query one known group directly by URL-encoding its name. For example, a group called Proxy can be requested as /proxies/Proxy; names containing spaces should be encoded as %20 or handled by an HTTP client that performs URL encoding automatically.
Reading status is also useful for diagnostics. The API can expose active connections, traffic counters, memory information, and configuration metadata depending on the core. These read operations should form the first stage of an automation system: discover the group, record the current member, collect candidate names, and only then make a controlled change.
Switch Nodes and Detect Unhealthy Proxies
For a manually selectable group, the usual switching operation is an HTTP PUT request to the group endpoint. The request body contains the exact proxy name to activate:
curl -X PUT \
-H "Authorization: Bearer $CLASH_SECRET" \
-H "Content-Type: application/json" \
--data '{"name":"Node-US-01"}' \
http://127.0.0.1:9090/proxies/Proxy
A successful response may contain no meaningful body. Verify the result by requesting the group again and checking its current member. If the request returns a client or server error, check whether the target is actually a member of that group, whether the group is automatic rather than manually selectable, and whether the URL encoded the group name correctly.
Latency testing is a separate operation. A common endpoint is:
curl -s \
-H "Authorization: Bearer $CLASH_SECRET" \
"http://127.0.0.1:9090/proxies/Node-US-01/delay?url=https%3A%2F%2Fwww.gstatic.com%2Fgenerate_204&timeout=5000"
The result typically includes a delay in milliseconds. A timeout, connection error, or non-success response should be treated as an unhealthy result for the purpose of failover, but latency alone does not prove that a node is suitable for every application. A node may answer a lightweight test quickly while failing large downloads, WebSocket connections, UDP traffic, or a destination with different TLS and routing characteristics.
Choose a test URL that is stable, small, and permitted in your environment. The endpoint should not require a login, should not return a large document, and should be reachable through the proxy protocol you want to validate. Keep the timeout realistic: a five-second timeout is useful for interactive recovery, while a longer timeout may be appropriate on a high-latency mobile connection.
Tip: separate health from preference
A healthy node is not automatically the best node. Keep a minimum health requirement, then rank successful candidates by delay, recent failure count, region, or application-specific preference. This prevents an unstable node from repeatedly winning because it happened to respond once.
A reliable failover loop should also include a cooldown. Without one, two nearly equal nodes can cause constant switching, which interrupts existing connections and makes troubleshooting difficult. Record the last switch time, require several consecutive failures before leaving the current node, and avoid switching again for a short interval unless the active node is completely unavailable.
Example failover sequence:
- Read the selected group and save its current member.
- Build a candidate list from the group’s returned members.
- Remove entries that are groups, disabled nodes, or names outside your allowlist.
- Test each remaining node with the same URL and timeout.
- Reject timeouts and apply a maximum acceptable delay.
- Choose the best candidate while respecting cooldown and failure history.
- Send the
PUTrequest to change the group member. - Read the group again and write an audit log containing the old and new names.
Here is a compact Python example that switches to the fastest candidate that passes a delay test:
import os
import time
import requests
BASE = "http://127.0.0.1:9090"
GROUP = "Proxy"
TEST_URL = "https://www.gstatic.com/generate_204"
headers = {"Authorization": f"Bearer {os.environ['CLASH_SECRET']}"}
proxies = requests.get(f"{BASE}/proxies", headers=headers, timeout=5).json()["proxies"]
group = proxies[GROUP]
candidates = [name for name in group.get("all", []) if name != "DIRECT"]
results = []
for name in candidates:
try:
response = requests.get(
f"{BASE}/proxies/{requests.utils.quote(name, safe='')}/delay",
params={"url": TEST_URL, "timeout": 5000},
headers=headers,
timeout=6,
)
response.raise_for_status()
delay = response.json().get("delay")
if isinstance(delay, int):
results.append((delay, name))
except (requests.RequestException, ValueError):
continue
if results:
delay, selected = min(results)
requests.put(
f"{BASE}/proxies/{requests.utils.quote(GROUP, safe='')}",
json={"name": selected},
headers=headers,
timeout=5,
).raise_for_status()
print(f"Selected {selected} at {delay} ms")
else:
raise SystemExit("No healthy candidate found")
This example is intentionally conservative. Production automation should add retries with backoff, structured logs, a maximum switch frequency, and a notification path. It should also distinguish an API failure from a proxy failure. If the controller itself cannot be reached, changing nodes is impossible; alert on that condition instead of treating every error as a bad node.
Integrate the API into Scripts and Deployment Pipelines
For cron jobs or scheduled tasks, keep the automation process independent from the GUI. The job can run a health check at fixed intervals, but it should not assume that the client window is visible or that the system proxy is enabled. The external controller operates on the running core, so a successful API switch changes the policy group even when the desktop interface is closed. The core must still be running, and the applications must actually use Clash through system proxy, TUN, redirection, or another supported traffic path.
In a CI or deployment pipeline, do not make a global node switch merely because one build experienced a network error. First determine whether the error came from DNS, an application timeout, a remote service outage, or the selected proxy. A safer design has a dedicated policy group for automation traffic, so a deployment script does not unexpectedly change the proxy used by browsers, games, or other users on the same machine.
- Store the controller secret in an environment variable or operating-system secret store.
- Restrict the controller port with a host firewall when remote access is unavoidable.
- Use HTTPS or a protected tunnel for remote administration if the client supports it; never send a secret across an untrusted plain-text network.
- Limit scripts to read-only endpoints unless a switch is genuinely required.
- Log timestamps, group names, candidate results, and decisions, but avoid logging subscription URLs or authentication headers.
- Validate node names against the current API response after every subscription update.
API compatibility deserves its own check. Mihomo and older Clash-based cores may expose different fields, support different group types, or add endpoints that another client does not implement. A robust script should inspect HTTP status codes, tolerate unknown JSON fields, and fail safely when the expected group is missing. Never interpret an empty candidate list as permission to select an arbitrary proxy or to rewrite the entire configuration.
Avoid configuration reload races
Subscription updates can temporarily replace proxy names while an automation job is reading them. If a switch fails immediately after an update, fetch the group again, wait for the profile to settle, and retry once. Do not repeatedly issue PUT requests against stale names.
When debugging, reproduce one action at a time. First call /version, then read /proxies, then inspect the target group, then test one node, and only after that send a switch request. Capture the HTTP status and response body from each step. This sequence quickly separates authentication mistakes, URL encoding problems, unsupported endpoints, missing group members, and genuinely unhealthy nodes.
Frequently Asked Questions
Which port does the Clash External Controller use?
There is no universal port. The port is the number configured after external-controller, commonly something like 9090 or another local service port. It is separate from the HTTP, SOCKS, or mixed proxy port. If you are unsure, inspect the active profile or the client’s advanced settings and test the controller with /version.
Why does the API return 401 or 403?
The request is usually missing the correct Bearer token, contains an outdated secret, or is being sent to a different Clash instance than the one you configured. Confirm the exact value of secret, avoid extra quotation marks in the environment variable, and send the header as Authorization: Bearer YOUR_SECRET. Also verify that a desktop client has not switched to another profile with different controller settings.
Can I manually switch a URLTest or Fallback group?
Automatic groups are designed to choose members according to their own rules, so manual selection may be rejected or may not behave as expected. If you need deterministic automation, create a dedicated selectable group and place the desired nodes inside it. You can still use delay tests in your script, while keeping the final choice explicit and auditable.
Will switching nodes preserve existing connections?
Not necessarily. Existing TCP sessions may remain attached to the previous outbound until they close, while new requests use the new selection. Some applications retry automatically; others fail once and require a restart. Design failover around new connection attempts and verify the application’s retry behavior instead of assuming that a policy change instantly migrates every active session.
Many lightweight proxy clients offer a convenient manual switch but become awkward when you need repeatable health checks, exact policy-group control, or integration with cron and deployment tools. Clash addresses those gaps with a documented controller model, runtime proxy inspection, selectable groups, and a broad ecosystem of desktop and Mihomo-compatible clients. If you want a more transparent starting point for testing node failover, you can Download Clash for free and connect the External Controller to your own scripts when you are ready.
Get the Most Stable Clash Experience
Download the latest Clash core optimized for 2026 network protocols. High speed, low latency, zero hassle.
Download Clash for Windows/macOS