Repository navigation
agents subman skill references server api
English | 简体中文
For full endpoint documentation, see docs/api/server-api.md. For schemas and
code generation, use docs/api/openapi.yaml. The staged design for future
subscription, aggregate, publication, and export routes is in
docs/api/roadmap.md. This file is the short agent-facing version for
automation work. For the complete sing-box-vps caller workflow, use
the integration guide
(简体中文).
The Server API is for owner-operated, trusted backend scripts. It runs inside the
same SvelteKit app on Cloudflare Workers and writes to the same Workspace Gist as
the browser UI through the same WorkspaceCoordinator Durable Object.
All endpoints except GET /api/health require:
Authorization: Bearer <SUBMAN_API_TOKEN>SUBMAN_API_TOKEN is separate from the GitHub token. It is one shared,
full-access bearer without scopes or per-client revocation. Backend scripts
should not hold GITHUB_TOKEN; they should receive the API token only over TLS,
keep it out of URLs and logs, and support rotation.
Use this for VPS installers and repeatable node sync:
PUT /api/nodes/by-key/:externalKeyOne stable external key addresses one node. The API stores the key as a tag label:
external:<externalKey>
Choose stable keys such as vps-1-vless, hostname-vless-reality, or
server-id-protocol-port. New/changed keys must fit the generated tag's
128-byte label limit: 119 UTF-8 bytes after the 9-byte external: prefix.
Leave one tag slot for that marker (at most 63 distinct caller tags). Encode the
key exactly once; literal percent sequences remain key data. Matching is
case-sensitive after trimming. Do not submit any reserved external: tags.
The endpoint is resource-identity idempotent, not request-replay idempotent.
Every successful call may update updatedAt and advance the Workspace revision.
Idempotency-Key is not supported.
PUT replaces all writable fields. Omitted tags, enabled, and source use create
defaults rather than retaining old values. PATCH with tags replaces the full
list and removes the key marker; use PUT by-key to maintain that identity.
After deletion a new by-key PUT can create a different node ID.
Example:
curl --fail-with-body -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"]}'{
"name": "vps-1 vless",
"type": "vless",
"raw": "vless://...",
"enabled": true,
"tags": ["sing-box-vps", "auto"],
"source": "single"
}Rules:
-
name,type, andraware required. - Duplicate names are automatically saved with a timestamp suffix, for example
HK 2026-05-26 06:32. - Duplicate trimmed
rawURIs are rejected with409 duplicate_node_raw. -
enableddefaults totrue. -
sourcedefaults tosingle. -
tagsdefaults to an empty array. -
tagsmay be strings or{ "id": "...", "label": "..." }objects.
Allowed type values:
vless, vmess, trojan, ss, ssr, hysteria2, tuic, anytls, other
-
GET /api/health: check Worker secret configuration. -
GET /api/nodes: list nodes. -
POST /api/nodes: create a new node, unless the raw URI already exists. -
GET /api/nodes/:id: get one node. -
PATCH /api/nodes/:id: update one node; duplicate names are made unique and raw URI collisions are rejected. -
DELETE /api/nodes/:id: delete one node and remove it from aggregatenodeIds. -
PUT /api/nodes/by-key/:externalKey: create/update the node identified by a stable key.
Only these health and node routes are public integration contracts. The browser
route /api/workspaces/:workspaceId/mutations is internal, uses a different
credential and envelope, and has no compatibility guarantee. Subscriptions,
aggregates, publication, and exports do not yet have public REST endpoints.
- A Node API write
2xxresponse means the coordinator committed and read-back verified the remote Workspace. Browser-only completion states such aspeer-ownedandretry-scheduleddo not apply to this API. - Successful node responses include the committed revision in the JSON body,
X-SubMan-Revision, and a strong ETag such as"subman-revision-12". - A write may send the last ETag as
If-Match. A stale value returns412 precondition_failedwith dispositionstate-conflictand the current revision. Re-read before deciding whether to reapply the intent. - Concurrent writes without
If-Matchmay still receive409 revision_conflictfrom the coordinator. - GET returns an observed snapshot, not a commit. It can create a bootstrap Gist
if discovery finds none; V1/bootstrap reads have revision 0. Discovery uses
Worker credentials, not the browser binding, and has no Workspace selector.
Multiple/invalid candidates return sanitized
502 gist_read_failed. Checkworkspace.gistIdbefore using an ETag; revision alone does not identify a Gist. - Health always returns HTTP 200; inspect
ok. It checks secret presence only, without testing GitHub permissions, discovery, or the coordinator binding. - Node writes leave existing published outputs unchanged. Publish in the browser after reviewing affected aggregates/exports.
Errors use { "error": { "code", "message", "disposition" } }. Branch on
code and disposition, never on message. Retryable GitHub failures may add a
bounded gateway object and Retry-After header; no raw upstream body is
returned.
-
GET: retry transport andretryable-upstreamfailures with bounded backoff. -
PUT .../by-key: retry only a confirmed non-commit retryable failure with bounded backoff. After an uncertain response, GET the list and find the exact external tag first; a blind replay can advance revision again. There is no GET-by-key. -
PATCH: re-read the node and revision before retrying an unknown outcome. -
POST: never blindly retry an unknown outcome because it may create another node. -
DELETE: re-read after an unknown outcome; a repeated delete may returnentity_deletedorentity_not_found.
retryable-upstream does not prove a write failed before commit: a PATCH timeout,
verification failure, or commit-index failure may follow a successful write.
Re-read uncertain outcomes and honor safe retry timing before resubmitting.
Domain limits use 400 invalid_mutation; request bodies above 9 MiB use
413 payload_too_large. 500 workspace_size_limit is operator-repair.
- Every write is a revisioned mutation. GitHub Gist remains a low-frequency storage backend even though the coordinator prevents lost updates.
- A first write may migrate V1 and create the immutable
subman.v1.backup.json; investigatemigration_backup_conflictrather than replacing the backup. - CORS is not broadly opened for arbitrary browser origins and is not an authentication boundary.
- Treat
409 duplicate_node_rawas an idempotency or inventory mismatch signal; inspect the existing node before retrying with a different raw URI. - Rotate
SUBMAN_API_TOKENif a trusted script host is compromised.
SubMan Wiki
| English | 简体中文 |
|---|---|
| User guide | 使用指南 |
| Development | 开发指南 |
| Server API | 接口文档 |
| Operations | 运维与恢复 |
| Migration | 旧版迁移 |
| sing-box export | sing-box 导出 |
| CI/CD | CI/CD |
| Roadmap | 路线图 |
| Changelog | 变更记录 |