SubMan Server API is an owner-operated REST API for trusted backend scripts.
The first version is designed for automation such as sing-box-vps installing
or updating nodes in the SubMan workspace.
The API runs inside the existing SvelteKit app on Cloudflare Workers. It writes to the same Workspace Gist used by the browser UI.
Use the origin where SubMan is deployed:
https://subman.example.com
Examples below use https://subman.example.com. Replace it with your deployed
SubMan domain.
All endpoints except GET /api/health require:
Authorization: Bearer <SUBMAN_API_TOKEN>SUBMAN_API_TOKEN is your own SubMan API token. It is separate from the GitHub
token and is safe to give to trusted automation scripts.
Before using the API in Cloudflare Workers, configure two secrets:
bun wrangler secret put GITHUB_TOKEN
bun wrangler secret put SUBMAN_API_TOKENGITHUB_TOKEN must have GitHub gist permission. The Worker uses it to find or
create the Workspace Gist and update subman.json.
SUBMAN_API_TOKEN is an arbitrary long secret value that callers must send in
the Authorization header.
Then deploy:
bun run build
bun run deployThe API uses the same workspace identity as the UI:
- Gist description:
SubMan-Data - Workspace file:
subman.json
If the Workspace Gist does not exist, the API creates it. Write operations read
the current subman.json, mutate the requested node data, then write the full
sync state back to the same file.
For JSON requests:
Authorization: Bearer <SUBMAN_API_TOKEN>
Content-Type: application/jsonThe API is intended for backend scripts. It does not enable broad browser CORS by default.
{
"id": "string",
"name": "vps-1 vless",
"type": "vless",
"raw": "vless://...",
"tags": [
{
"id": "sing-box-vps",
"label": "sing-box-vps"
}
],
"enabled": true,
"updatedAt": "2026-05-06T00:00:00.000Z",
"source": "single"
}Allowed type values:
vlessvmesstrojanssssrhysteria2tuicanytlsother
Allowed source values:
singlesubscription
POST /api/nodes and PUT /api/nodes/by-key/:externalKey accept:
{
"name": "vps-1 vless",
"type": "vless",
"raw": "vless://...",
"enabled": true,
"tags": ["sing-box-vps", "auto"],
"source": "single"
}Rules:
nameis required.- If another node already has the same name, the API saves the node with a
timestamp suffix such as
HK 2026-05-26 06:32. typeis required and must be one of the allowed proxy types.rawis required.- If another node already has the same trimmed
rawURI, the API rejects the write with409 duplicate_node_raw. enableddefaults totrue.sourcedefaults tosingle.tagsdefaults to an empty array.tagsmay be strings or{ "id": "...", "label": "..." }objects.
PATCH /api/nodes/:id accepts any subset of the node create body:
{
"name": "vps-1 renamed",
"enabled": false
}Missing fields keep their existing values.
Patch requests use the same duplicate handling as create requests: duplicate names are made unique, and duplicate raw URIs on another node are rejected.
List responses:
{
"data": [],
"workspace": {
"gistId": "gist-id",
"file": "subman.json"
}
}Single-node write responses:
{
"data": {
"id": "node-id",
"name": "vps-1 vless",
"type": "vless",
"raw": "vless://...",
"tags": [],
"enabled": true,
"updatedAt": "2026-05-06T00:00:00.000Z",
"source": "single"
},
"workspace": {
"gistId": "gist-id",
"file": "subman.json"
}
}Error responses:
{
"error": {
"code": "unauthorized",
"message": "Unauthorized"
}
}GET /api/healthAuthentication: not required.
Returns whether the required server-side secrets are configured. Secret values are never returned.
Example:
curl -sS "https://subman.example.com/api/health"Response:
{
"ok": true,
"config": {
"githubToken": true,
"submanApiToken": true
}
}GET /api/nodesExample:
curl -sS "https://subman.example.com/api/nodes" \
-H "Authorization: Bearer $SUBMAN_API_TOKEN"Response:
{
"data": [
{
"id": "node-id",
"name": "vps-1 vless",
"type": "vless",
"raw": "vless://...",
"tags": [],
"enabled": true,
"updatedAt": "2026-05-06T00:00:00.000Z",
"source": "single"
}
],
"workspace": {
"gistId": "gist-id",
"file": "subman.json"
}
}POST /api/nodesCreates a new node every time unless the submitted raw URI already exists. If
the submitted name already exists, the saved name receives a timestamp suffix.
For installer scripts, prefer PUT /api/nodes/by-key/:externalKey to avoid
creating separate records for the same machine-managed node.
Example:
curl -sS -X POST "https://subman.example.com/api/nodes" \
-H "Authorization: Bearer $SUBMAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["manual"]}'Success status: 201 Created.
GET /api/nodes/:idExample:
curl -sS "https://subman.example.com/api/nodes/node-id" \
-H "Authorization: Bearer $SUBMAN_API_TOKEN"PATCH /api/nodes/:idExample:
curl -sS -X PATCH "https://subman.example.com/api/nodes/node-id" \
-H "Authorization: Bearer $SUBMAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled":false,"name":"vps-1 disabled"}'DELETE /api/nodes/:idDeletes the node and removes its id from aggregate rule nodeIds.
Example:
curl -sS -X DELETE "https://subman.example.com/api/nodes/node-id" \
-H "Authorization: Bearer $SUBMAN_API_TOKEN"Response:
{
"data": {
"deleted": true
},
"workspace": {
"gistId": "gist-id",
"file": "subman.json"
}
}PUT /api/nodes/by-key/:externalKeyThis is the recommended endpoint for automation scripts. It is idempotent: running the same command again updates the existing node instead of creating a duplicate.
It still follows normal node validation: duplicate names are made unique, and a
raw URI that already belongs to a different node is rejected with
409 duplicate_node_raw.
The API stores the external key as a hidden-style tag label:
external:<externalKey>
Choose a stable externalKey, for example:
vps-1-vlesshostname-vless-realityserver-id-protocol-port
Example:
curl -sS -X PUT "https://subman.example.com/api/nodes/by-key/vps-1-vless" \
-H "Authorization: Bearer $SUBMAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"vps-1 vless","type":"vless","raw":"vless://...","enabled":true,"tags":["sing-box-vps"]}'#!/usr/bin/env bash
set -euo pipefail
SUBMAN_BASE_URL="https://subman.example.com"
SUBMAN_API_TOKEN="${SUBMAN_API_TOKEN:?SUBMAN_API_TOKEN is required}"
HOSTNAME="$(hostname)"
NODE_KEY="${HOSTNAME}-vless-reality"
NODE_NAME="${HOSTNAME} vless reality"
NODE_RAW="vless://..."
curl -fsS -X PUT "${SUBMAN_BASE_URL}/api/nodes/by-key/${NODE_KEY}" \
-H "Authorization: Bearer ${SUBMAN_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\":\"${NODE_NAME}\",\"type\":\"vless\",\"raw\":\"${NODE_RAW}\",\"enabled\":true,\"tags\":[\"sing-box-vps\",\"auto\"]}"| Status | Code | Meaning |
|---|---|---|
200 |
- | Read, update, delete, or upsert succeeded. |
201 |
- | Node created by POST /api/nodes. |
400 |
bad_request |
Invalid JSON body or unsupported field value. |
401 |
unauthorized |
Missing or invalid SUBMAN_API_TOKEN. |
409 |
duplicate_node_raw |
Submitted raw URI already belongs to another node. |
404 |
not_found |
Requested node id does not exist. |
500 |
server_error |
Missing GITHUB_TOKEN or unexpected server/Gist failure. |
- Keep
GITHUB_TOKENonly in Cloudflare Secrets. - Give backend scripts only
SUBMAN_API_TOKEN. - Rotate
SUBMAN_API_TOKENif a script host is compromised. - The Gist backend is suitable for low-frequency automation. It is not intended for high-concurrency writes from many machines at the same time.
- CORS is intentionally not opened for arbitrary browser origins in this API version.