| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
— | Web UI |
| GET | /sub |
token | Base64 subscription content |
Most /api/* endpoints require a valid session token. Exceptions: /api/login (creates session) and /api/export/momo, /api/export/kernel, /api/export/windows, /api/export/android (also accept sub-token via ?token=).
| Method | Path | Description |
|---|---|---|
| POST | /api/login |
Returns session token |
| POST | /api/logout |
Invalidates session |
| GET | /api/nodes |
List env + stored nodes |
| PUT | /api/nodes |
Save stored nodes (replaces all) |
| GET | /api/export/sing-box |
Export all nodes as sing-box JSON |
| GET | /api/export/momo |
Export full config.json for OpenWrt-momo |
| GET | /api/export/kernel |
Export full config.json for HPC/desktop sing-box |
| GET | /api/export/windows |
Export full config.json for Windows desktop sing-box |
| GET | /api/export/android |
Export full config.json for Android (SFA) |
| GET | /api/sub-url |
Returns the primary subscription URL |
| PUT | /api/sub-token |
Rotate the primary subscription token |
| GET | /api/sub-tokens |
List all tokens (primary + scoped) |
| POST | /api/sub-tokens |
Create a scoped token |
| POST | /api/sub-tokens/rotate |
Rotate a scoped token's value |
| PUT | /api/sub-tokens |
Update a scoped token |
| DELETE | /api/sub-tokens |
Delete a scoped token |
| GET | /api/audit-log |
List audit log entries (500 max) |
| DELETE | /api/audit-log |
Clear all audit log entries |
| GET | /api/upload-token |
View current upload token |
| PUT | /api/upload-token |
Rotate upload token |
| GET | /api/upstream |
List upstream subscription sources |
| POST | /api/upstream |
Add upstream subscription source |
| DELETE | /api/upstream |
Remove upstream source (?id=) |
| POST | /api/upstream/sync |
Sync upstream source(s) |
Scoped tokens allow sharing specific nodes with different people. Each scoped token has:
name— optional display namenodes— array of node URIs this token can access
When a scoped token is used with /sub?token=<scoped>, only the assigned nodes are returned.
The primary token (set via SUB_TOKEN env var, sub:token KV key, or rotated via /api/sub-token) grants access to all nodes.
POST /api/upload pushes node URIs into the stored node pool. It uses its own token (UPLOAD_TOKEN env var), independent from session or sub-token auth.
Auth: ?token=<upload_token> query param. If UPLOAD_TOKEN is not configured, returns 403 Upload not enabled.
Request: { "nodes": ["vless://...", "vmess://..."] }
Response: { "ok": true, "added": 5, "dupes": 2 }
Behavior:
- Only valid scheme URIs are accepted (same as
isValidNode) - Exact duplicates (already in store) are skipped and counted as
dupes - Nodes with the same
#fragmentname get a-2/-3suffix appended - Uploads are recorded in the audit log as
upload - Invalid tokens are rate-limited (shares the global 10/15min counter)
Scoped tokens allow sharing specific nodes with different people. Each scoped token has:
name— optional display namenodes— array of node URIs this token can access
When a scoped token is used with /sub?token=<scoped>, only the assigned nodes are returned.
The primary token (set via SUB_TOKEN env var, sub:token KV key, or rotated via /api/sub-token) grants access to all nodes.
Create a new scoped token.
// Request
{ "name": "Friend A", "nodes": ["vless://abc@1.1.1.1:443#Tokyo"] }
// Response
{ "token": "<48-char-hex>", "name": "Friend A", "nodes": ["vless://abc@1.1.1.1:443#Tokyo"] }Update a scoped token's name and/or node list.
// Request
{ "token": "<48-char-hex>", "name": "New Name", "nodes": ["vless://..."] }
// Response
{ "token": "<48-char-hex>", "name": "New Name", "nodes": ["vless://..."] }Rotate a scoped token — generates a new random token value while preserving name and node assignments.
// Request
{ "token": "<48-char-hex>" }
// Response
{ "token": "<new-48-char-hex>", "name": "Friend A", "nodes": ["vless://..."] }Delete a scoped token. Token passed as query parameter.
DELETE /api/sub-tokens?token=<48-char-hex>
→ { "ok": true }
| Parameter | Description |
|---|---|
?token= |
Primary token → all nodes, scoped token → assigned nodes |
- No
SUB_TOKENset →/subis public (all nodes, no token needed) SUB_TOKENset →/subrequires?token=<primary>for all nodes, or?token=<scoped>for filtered- Invalid tokens return
401and are rate-limited (shared with login brute-force: 10 attempts / 15 min per IP) - Response:
Content-Type: text/plain, Base64-encoded, one URI per line
POST /api/login: 10 wrong attempts / 15 min per IPGET /sub: invalid tokens count toward the same 10-attempt / 15 min per IP limitGET /api/export/momo,GET /api/export/kernel: invalid?token=also shares the same counterPOST /api/upload: invalid?token=shares the same counter; valid upload clears it- After 10 failures, returns
429 Too many requests - Only login or valid upload/export access clears the counter
- All rate-limits share the same global counter per IP
Exports all configured nodes as a sing-box-compatible outbounds JSON array. Requires admin session.
Response:
{
"ok": true,
"count": 3,
"outbounds": [
{
"type": "vless",
"tag": "Tokyo-01",
"server": "1.2.3.4",
"server_port": 443,
"uuid": "...",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "s0.awsstatic.com",
"utls": { "enabled": true, "fingerprint": "firefox" },
"reality": {
"enabled": true,
"public_key": "...",
"short_id": "..."
}
}
}
],
"errors": []
}Supported URL schemes (auto-detected from node URI):
| Scheme | sing-box type |
Notes |
|---|---|---|
vless:// |
vless |
Reality/TLS, ws/grpc/h2/tcp transport |
vmess:// |
vmess |
Base64 JSON decoding, ws/grpc/h2 transport |
trojan:// |
trojan |
ws/grpc transport, multiplex |
ss:// |
shadowsocks |
SIP002 + legacy format |
hysteria2:// / hy2:// |
hysteria2 |
TLS insecure toggle |
tuic:// |
tuic |
ALPN, congestion_control |
anytls:// |
anytls |
Reality with utls fingerprint |
naive:// |
naive |
HTTP/3 proxy |
The returned JSON can be directly merged into a sing-box client config:
{
"outbounds": [ <paste the outbounds array here> ]
}Returns a complete sing-box config.json compatible with luci-app-momo on OpenWrt. The config can be used directly as a subscription URL in momo's "Subscription" profile mode, or downloaded and placed in the "File" profile.
Auth: Two modes supported:
- Session token (
Authorization: Bearer <token>) — for Web UI download - Sub-token (
?token=<sub_token>) — for momo's curl subscription (also supports scoped tokens)
Momo subscription URL:
https://your-domain.com/api/export/momo?token=<your_sub_token>
If you use scoped tokens, replace <your_sub_token> with a scoped token to filter nodes. Add &preset=ipv4plus_realip for dual-stack with real DNS, or &preset=ipv4plus_fakeip for dual-stack with FakeIP.
| Param | Default | Description |
|---|---|---|
preset |
ipv4only_realip |
Preset (see below) |
selectorTag |
GLOBAL |
Selector outbound tag name |
redirectPort |
7890 | Redirect inbound port |
tproxyPort |
7891 | TPROXY inbound port |
dnsPort |
1053 | DNS inbound port |
tunAddress |
172.31.0.1/30 |
TUN interface IPv4 address |
tunAddress6 |
— | TUN interface IPv6 address (auto for dual-stack) |
dnsStrategy |
preset-dependent | DNS strategy (ipv4_only / prefer_ipv4) |
listen |
preset-dependent | Inbound listen address (0.0.0.0 / ::) |
fakeip |
preset-dependent | Override: true/1 force FakeIP, false/0 force real DNS |
clashPort |
9095 | Clash API listen port |
clashSecret |
"" |
Clash API secret |
| Preset | listen | dnsStrategy | FakeIP | TUN v6 |
|---|---|---|---|---|
ipv4only_realip |
0.0.0.0 |
ipv4_only |
no | no |
ipv4only_fakeip |
0.0.0.0 |
ipv4_only |
yes | no |
ipv4plus_realip |
:: |
prefer_ipv4 |
no | yes |
ipv4plus_fakeip |
:: |
prefer_ipv4 |
yes | yes |
Aliases (for backward compatibility): ipv4only / ipv4 / single → ipv4only_realip; ipv4+6 / dual / ipv6 → ipv4plus_realip.
The response is a raw sing-box config.json — no wrapper, ready for momo to use directly.
{
"log": { "disabled": false, "level": "info", "timestamp": true },
"dns": { ... },
"ntp": { ... },
"inbounds": [ ... ],
"outbounds": [ ... ],
"route": { ... },
"experimental": { ... }
}The response includes:
log: logging config (disabled: false, level: info, timestamp: true)dns: local UDP → ali DoH → Google DoH; FakeIP server + A/AAAA rules only if preset enables it (or?fakeip=trueoverrides)ntp: time sync (time.apple.com:123, 30m interval)inbounds:dns-in(direct:1053),redirect-in(redirect:7890),tproxy-in(tproxy:7891),tun-in(tun, momo device)outbounds: all converted nodes + aselectoroutbound containing all node tags +directfor bypassroute: sniff → hijack-dns → private-ip → geosite-cn → geoip-cn → final to selectorexperimental: cache_file (fakeip persistence) + clash_api (Yacd-meta dashboard on port 9095)
Returns a complete sing-box config.json for the sing-box kernel on Linux desktop / HPC. Drop into /etc/sing-box/config.json or point sing-box run -c at it.
Auth: Same two modes as /api/export/momo (session token or sub-token).
Kernel subscription URL:
https://your-domain.com/api/export/kernel?token=<your_sub_token>
| Param | Default | Description |
|---|---|---|
preset |
ipv4only_realip |
Preset (see below) |
selectorTag |
GLOBAL |
Selector outbound tag name |
dnsPort |
1053 | DNS inbound port |
mixedPort |
7890 | HTTP/SOCKS mixed inbound port |
tunAddress |
172.19.0.1/30 |
TUN interface IPv4 address |
tunAddress6 |
— | TUN interface IPv6 address (auto for dual-stack) |
dnsStrategy |
preset-dependent | DNS strategy (ipv4_only / prefer_ipv4) |
listen |
preset-dependent | Inbound listen address (0.0.0.0 / ::) |
fakeip |
preset-dependent | Override: true/1 force FakeIP, false/0 force real DNS |
clashPort |
9191 | Clash API listen port |
clashSecret |
"" |
Clash API secret |
tunName |
stun |
TUN interface name |
Same 4 presets as momo: ipv4only_realip, ipv4only_fakeip, ipv4plus_realip, ipv4plus_fakeip.
{
"log": { "disabled": false, "level": "info", "timestamp": true },
"dns": { ... },
"inbounds": [ ... ],
"outbounds": [ ... ],
"route": { ... },
"experimental": { ... }
}The response includes:
log: logging config (disabled: false, level: info, timestamp: true)dns: local UDP → ali DoH → Google DoH; FakeIP server + A/AAAA rules if preset enables itinbounds:dns-in(direct:1053),tun-in(tun, stun device, auto_route),mixed(HTTP/SOCKS proxy)outbounds: all converted nodes + aselectoroutbound containing all node tags +directfor bypassroute: hijack-dns → private-ip → geosite-cn → geoip-cn → sniff → final to selector; auto_detect_interfaceexperimental: cache_file + clash_api (Yacd-meta dashboard on port 9191)
Returns a complete sing-box config.json for the official sing-box for Android app (SFA). The TUN device is provided by Android VpnService, so interface_name is omitted and per-app proxy is controlled through include_package / exclude_package.
Auth: Same two modes as /api/export/momo (session token or sub-token).
Android subscription URL:
https://your-domain.com/api/export/android?token=<your_sub_token>
Same as /api/export/kernel, plus:
| Param | Default | Description |
|---|---|---|
includePackage |
— | Comma-separated Android package names to proxy |
excludePackage |
— | Comma-separated Android package names to bypass |
overrideAndroidVpn |
false |
1/true sets route.override_android_vpn (accept another VPN as upstream) |
clashPort |
9090 | Clash API listen port |
Same 4 presets as the other platforms: ipv4only_realip, ipv4only_fakeip, ipv4plus_realip, ipv4plus_fakeip.
Same shape as /api/export/kernel. The response includes:
log,dns,outbounds,http_clients: same as/api/export/windowsinbounds:dns-in(direct:1053),tun-in(tun, nointerface_name,auto_route+strict_route),mixed(7890, noset_system_proxy)route: sniff → hijack-dns → private-ip → geosite-cn → geoip-cn → final to selector;auto_detect_interfaceexperimental: cache_file (cache.db) + clash_api (Yacd-meta dashboard on port 9090)
Returns recent audit entries (newest first, max 500).
Auth: Session token required.
Response:
{
"log": [
{
"ts": 1700000000000,
"action": "login",
"level": "INFO",
"ip": "1.2.3.4",
"detail": ""
}
]
}| Action | Level | Detail | Trigger |
|---|---|---|---|
login |
INFO | — | Successful admin login |
login-failed |
WARN | — | Wrong password |
blocked |
WARN | endpoint name | Rate-limited (429) |
logout |
INFO | — | Session logout |
sub |
INFO | N nodes |
Subscription accessed |
nodes-save |
INFO | N nodes |
Nodes updated via UI |
token-create |
INFO | token name | Scoped token created |
token-update |
INFO | token prefix | Scoped token modified |
token-rotate |
INFO | token name / prefix | Token rotated |
token-delete |
INFO | token prefix | Scoped token deleted |
export-momo |
INFO | N nodes |
Momo config exported |
export-kernel |
INFO | N nodes |
Kernel config exported |
export-windows |
INFO | N nodes |
Windows config exported |
export-android |
INFO | N nodes |
Android (SFA) config exported |
export-json |
INFO | N outbounds |
Sing-box JSON exported |
upload |
INFO | added/dupes | Node uploaded via API |
upstream-add |
INFO | name/URL | Upstream source added |
upstream-sync |
INFO / ERROR | N nodes |
Upstream sync completed (ERROR if any source failed) |
upstream-delete |
INFO | name/URL | Upstream source deleted |
Clears all audit entries. Session auth required.
Audit log is stored under the audit:log KV key as a JSON array. Entries are capped at 500 — oldest entries are dropped when the limit is exceeded. No TTL — entries persist until manually cleared or evicted by the cap.
Imports external subscription URLs into subhatch for use in momo/kernel exports. Upstream nodes are merged into the GLOBAL selector alongside stored nodes, but are NOT visible to scoped tokens (primary token only).
List all upstream sources with sync status. Session auth.
Response:
{
"urls": [
{ "name": "Airport A", "url": "https://...", "hash": "a1b2", "lastSync": 1700000000000, "lastError": null, "nodeCount": 12 }
]
}Add a new upstream source. Performs an initial sync on creation. Session auth.
Request: { "url": "https://...", "name": "My Source" } (name optional)
Response: { "ok": true, "entry": { ... } } (entry contains sync result)
Remove an upstream source and its cached nodes. Session auth.
Query: ?id=<index> — index as returned in the urls array
Response: { "ok": true }
Pull and refresh node cache for one or all upstream sources. Session auth.
Query: ?id=<index> to sync one, or omit to sync all
Response (single): { "ok": true, "count": 12 }
Response (all): { "results": [{ "name": "...", "ok": true, "count": 12 }, ...] }
Behavior:
- Fetches the URL, tries base64 decode first, falls back to plain text
- Filters to valid node URIs only (
vless://,vmess://, etc.) - Full replacement each sync (not append)
- Failed syncs retain the last-known-good cache
Upstream sources are synced on /api/export/* requests (momo, kernel, windows, android — freshness-first design), with a 60s minimum interval between syncs of the same source to avoid duplicate concurrent fetches. Add ?refresh=0 to skip the sync entirely.
If any source fails to sync, the response includes an X-Upstream-Warning header (e.g. 1/2 upstream sources failed to sync, serving cached nodes) and previously cached nodes are served instead.
Source list stored under upstream:urls KV key. Per-source node cache under upstream:nodes:<hash>.