36 KiB
RustDesk Console API — Backend Specification
This document specifies every HTTP/HTTPS endpoint that the RustDesk client in this
workspace calls against its server-side Console / API server (the API surface
exposed by RustDesk Server Pro / hbbs). The intent is that a backend implemented
to this spec will be able to serve a stock RustDesk client with no client-side
changes.
The client base URL is whatever the user / installer has configured as
api-server (or, falling back, derived from custom-rendezvous-server). All
paths below are relative to that base URL.
If the configured API host string contains "rustdesk.com/" or ends with
"rustdesk.com", the client treats it as the public/managed server and
suppresses the heartbeat, sysinfo, and audit endpoints
(is_public() in src/common.rs:1088).
A self-hosted backend MUST NOT use a hostname matching that pattern.
1. Conventions
1.1 Transport
- HTTPS expected in production. The client probes TLS (
rustls-tlsfirst, thennative-tls, then withdanger_accept_invalid_certs) on the first call and caches the result. See src/hbbs_http/http_client.rs. - A non-200 response body that is valid JSON with an
"error"key is treated as a structured error. SeeHbbHttpResponse::parsein src/hbbs_http.rs:24-39:{ "error": "human readable message" } - Default request timeout: 12 seconds (src/common.rs:1431).
- Logout has a hard 2 second timeout (flutter/lib/models/user_model.dart:168).
- The plugin-sign call uses 10 seconds (src/plugin/callback_msg.rs:293).
1.2 Headers
For every authenticated request the client adds:
Authorization: Bearer <access_token>
Content-Type: application/json
Authorization is built by getHttpHeaders() in flutter/lib/common.dart:2691-2695
from the locally stored access_token. Unauthenticated calls (login, OIDC,
heartbeat, sysinfo, audit, record, plugin-sign) omit Authorization.
1.3 Error envelope
Successful action endpoints (peer add/update/delete, tag CRUD, etc.) typically
return either an empty body / null / {} on success, or
{ "error": "<message>" } on failure with HTTP 200. The status code alone
is not a sufficient success indicator — the client always inspects the JSON
error field.
1.4 Pagination
List endpoints use 1-indexed pagination, query parameters:
?current=<page>&pageSize=<size>
The client always uses pageSize=100 and iterates pages until
current * pageSize >= total. Response:
{
"total": <int>,
"data": [ ... ]
}
1.5 IDs and identifiers
id— RustDesk peer/device ID (the 9-digit number shown in the UI).uuid— A per-install machine identifier, base64-encoded by the client before transmission (crate::encode64(hbb_common::get_uuid())).guid— Server-assigned GUID for an address book.conn_id,session_id— Internal ints assigned by the client per remote session, echoed back in audit logs.
1.6 deviceInfo object
Sent inside login bodies. Shape (from src/hbbs_http/account.rs:31-44):
{
"os": "Linux | Windows | macOS | Android | iOS",
"type": "client | browser",
"name": "<device name from client, or browser name+version>"
}
2. Base URL & API discovery
The client resolves its base URL via
get_api_server() in src/common.rs; a custom-rendezvous
server with no explicit api-server is mapped to
https://<rendezvous-host> (port 21114 if specified inline).
The client makes a HEAD request against <base>/api/login-options on
startup as a connectivity / TLS-detection probe
(src/hbbs_http/account.rs:155,
src/hbbs_http/record_upload.rs:34).
A backend SHOULD respond 200 to a HEAD on /api/login-options.
3. Authentication
3.1 POST /api/login
User credential / 2FA / SSO completion.
- Source: flutter/lib/models/user_model.dart:178-202
- Auth: none
- Headers:
Content-Type: application/json(sent implicitly byhttp.post)
Request body (from LoginRequest.toJson() at
flutter/lib/common/hbbs/hbbs.dart:133-178):
{
"username": "string", // optional
"password": "string", // optional
"id": "string", // RustDesk peer ID
"uuid": "string", // base64
"autoLogin": true, // optional bool
"type": "account | mobile | sms_code | email_code | tfa_code | oidc/<provider>",
"verificationCode": "string", // optional, for email/SMS challenge
"tfaCode": "string", // optional
"secret": "string", // optional, echoed back from a tfa_check response
"deviceInfo": { "os": "...", "type": "...", "name": "..." }
}
type constants
(flutter/lib/common/hbbs/hbbs.dart:11-15):
account, mobile, sms_code, email_code, tfa_code. oidc/<name> is
also accepted (the oidc/ prefix triggers the device-flow described in §3.5).
Successful response — LoginResponse shape
(flutter/lib/common/hbbs/hbbs.dart:180-197):
{
"access_token": "opaque-bearer-token",
"type": "access_token",
"tfa_type": "totp | ...", // present when type == "tfa_check"
"secret": "string", // present when type == "tfa_check"
"user": {
"name": "string",
"display_name": "string",
"avatar": "string",
"email": "string",
"note": "string",
"status": 1, // -1=unverified, 0=disabled, 1=normal
"is_admin": false,
"verifier": "string" // optional, used by web build only
}
}
Challenge responses. When 2FA / verification is required, type is one of
(flutter/lib/common/hbbs/hbbs.dart:17-19):
type |
Meaning |
|---|---|
access_token |
Login complete, access_token populated. |
email_check |
Server emailed a code; client must POST again with type:"email_code" and verificationCode. |
tfa_check |
TOTP required; server returns tfa_type and secret to echo back with type:"tfa_code" + tfaCode. |
Status / error semantics:
- Non-200 with body
{"error": "..."}→ client surfaces the error. - 401 from this endpoint is treated as bad credentials (no auto-logout).
- 401 from any other authenticated endpoint clears the local token (see §3.6).
3.2 GET /api/login-options
Returns the list of login methods the server is configured to expose.
- Source: flutter/lib/models/user_model.dart:222-245
- Auth: none
- Method:
GET(no body)
Response — JSON array of strings:
[
"account",
"oidc/google",
"oidc/github",
"common-oidc/[ {\"name\":\"google\",\"icon\":\"...\"} ]"
]
Two recognised conventions:
oidc/<provider>— exposed in the UI as an SSO button labelled<provider>.common-oidc/<json-array>— the suffix is a JSON-encoded array of provider descriptors{ "name": "...", ... }. If present anywhere in the list, it takes precedence over individualoidc/...entries.
3.3 POST /api/currentUser
Refreshes the cached profile of the currently logged-in user.
- Source: flutter/lib/models/user_model.dart:60-99
- Auth: Bearer
Request body:
{ "id": "<peer id>", "uuid": "<base64 uuid>" }
Response: UserPayload (see §3.1). On HTTP 401 or 400 the client clears
the access token (401 also wipes the address-book and group caches).
3.4 POST /api/logout
- Source: flutter/lib/models/user_model.dart:155-175
- Auth: Bearer
- Timeout: 2 s; failures are silently ignored client-side.
Request body:
{ "id": "<peer id>", "uuid": "<base64 uuid>" }
Response: body is ignored.
3.5 OIDC / SSO device flow
Two endpoints implement a polled device-code flow.
POST /api/oidc/auth
- Source: src/hbbs_http/account.rs:160-176
- Auth: none
Request body:
{
"op": "<provider name>", // matches an entry from /api/login-options
"id": "<peer id>",
"uuid": "<base64 uuid>",
"deviceInfo": { "os": "...", "type": "...", "name": "..." }
}
Response — OidcAuthUrl (src/hbbs_http/account.rs:24-28):
{
"code": "opaque-poll-handle",
"url": "https://server/oidc/redirect?code=...&..."
}
The client opens url in the user's browser and polls (§ next).
GET /api/oidc/auth-query
- Source: src/hbbs_http/account.rs:178-202
- Auth: none
- Polling: 1 s interval, 180 s timeout (
QUERY_TIMEOUT_SECS = 60*3).
Query parameters: code (from auth response), id, uuid.
Response wrapper — the response is an outer envelope whose body field is
itself JSON:
{
"body": "<stringified JSON response>"
}
The inner body, once parsed, is either:
- A normal
AuthBody(same shape as §3.1 success), or { "error": "No authed oidc is found" }while waiting (the client keeps polling until success or timeout), or- Any other
{ "error": "..." }(terminates polling with an error).
3.6 401 handling
A 401 from any authenticated endpoint causes the Flutter client to:
- Clear
access_tokenand cacheduser_infofrom local config. - Reset address-book and group state.
A 400 from /api/currentUser clears the access token but does not reset other state
(flutter/lib/models/user_model.dart:81-83).
4. Address book
The client supports two modes:
- Legacy mode — the whole address book is GET/PUT as a single JSON blob.
The server returns 404 on
/api/ab/personalto signal legacy mode (flutter/lib/models/ab_model.dart:271-274). - Shared mode — the personal AB and zero or more shared address books are
represented as separate
guid-keyed objects with paginated peer/tag lists.
A backend SHOULD implement shared mode (legacy mode is supported only as fall-back).
4.1 POST /api/ab/settings
Capability/limits probe. Called once per pull cycle.
- Source: flutter/lib/models/ab_model.dart:230-258
- Auth: Bearer
- Request body: empty (Content-Type still
application/json) - Special status:
404⇒ "this server does not support shared AB; abort init". Any other non-200 surfacespull_ab_failed_tip.
Response:
{ "max_peer_one_ab": 100 }
4.2 POST /api/ab/personal
Returns the GUID of the caller's personal address book.
- Source: flutter/lib/models/ab_model.dart:262-293
- Auth: Bearer
- Request body: empty
- Special status:
404⇒ legacy mode; client falls back to §4.7/§4.8.
Response:
{ "guid": "personal-ab-guid" }
4.3 POST /api/ab/shared/profiles
Paginated list of shared address books visible to the user.
- Source: flutter/lib/models/ab_model.dart:295-360
- Auth: Bearer
- Method:
POST(the resource is fetched with POST in the Flutter client, query params are still used for pagination) - Query:
current,pageSize - Request body: empty
- Special status:
404⇒ no shared-AB support.
Response — page of AbProfile:
{
"total": 12,
"data": [
{
"guid": "ab-guid",
"name": "Engineering",
"owner": "alice",
"note": "optional string",
"rule": 2, // 1=read, 2=read/write, 3=full control
"info": { ... } // opaque, surfaced in the UI as-is
}
]
}
rule enum from flutter/lib/common/hbbs/hbbs.dart:210-256.
4.4 POST /api/ab/peers?ab=<guid>
Paginated list of peers in a given address book.
- Source: flutter/lib/models/ab_model.dart:1432-1497
- Auth: Bearer
- Method:
POST - Query:
current,pageSize,ab=<guid> - Request body: empty
Response page entry — Peer:
{
"id": "123456789",
"alias": "string",
"tags": ["tag1", "tag2"],
"note": "string",
"hash": "string", // present in personal AB only (password hash)
"password": "string", // present in shared AB only (encrypted password)
"username": "string", // OS username on the remote device
"hostname": "string",
"platform": "Windows | Linux | Mac OS | Android | iOS"
}
hash/password are mutually exclusive: the client strips password for personal
ABs and hash for shared ABs before pushing
(flutter/lib/models/ab_model.dart:1561-1565).
4.5 POST /api/ab/tags/{guid}
List the tags of an address book. Returns a JSON array (no pagination).
- Source: flutter/lib/models/ab_model.dart:1499-1544
- Auth: Bearer
- Method:
POST - Request body: empty
Response:
[
{ "name": "Production", "color": -16776961 },
{ "name": "QA", "color": -65536 }
]
color is a Flutter Color.value (signed 32-bit ARGB packed integer).
4.6 Peer mutations on a shared/personal AB
All four take Authorization + Content-Type: application/json. Success is
either an empty body or {}; failure is HTTP 200 with {"error":"..."} (the
client reads error regardless of status).
POST /api/ab/peer/add/{guid} — add a peer
Source: flutter/lib/models/ab_model.dart:1548-1578.
{
"id": "123456789",
"alias": "string",
"tags": ["..."],
"note": "string",
"password": "string", // shared AB
"hash": "string", // personal AB
"username": "string",
"hostname": "string",
"platform": "string"
}
The client adds peers one-by-one (one HTTP call per peer).
PUT /api/ab/peer/update/{guid} — partial peer update
Source: flutter/lib/models/ab_model.dart:1580-1729.
The body always contains id, plus any subset of mutable fields. The client uses
this single endpoint for: alias change, note change, tag change, password
(password) / hash (hash) change, and username/hostname/platform sync
from recent connections.
{
"id": "123456789",
"alias": "string",
"tags": ["..."],
"note": "string",
"password": "string",
"hash": "string",
"username": "string",
"hostname": "string",
"platform": "string"
}
DELETE /api/ab/peer/{guid} — bulk delete
Source: flutter/lib/models/ab_model.dart:1751-1771.
Body is a JSON array of peer IDs:
["123456789", "987654321"]
4.7 Tag mutations on a shared/personal AB
POST /api/ab/tag/add/{guid}
Source: flutter/lib/models/ab_model.dart:1775-1802. The client iterates one POST per tag.
{ "name": "tag", "color": -16776961 }
PUT /api/ab/tag/rename/{guid}
Source: flutter/lib/models/ab_model.dart:1804-1831.
{ "old": "old-name", "new": "new-name" }
PUT /api/ab/tag/update/{guid} — set color
Source: flutter/lib/models/ab_model.dart:1833-1855.
{ "name": "tag", "color": -16776961 }
DELETE /api/ab/tag/{guid} — bulk delete
Source: flutter/lib/models/ab_model.dart:1857-1876.
Body is a JSON array of tag names: ["tagA", "tagB"].
4.8 Legacy address book (single-blob mode)
Used only when /api/ab/personal returns 404.
GET /api/ab
Source: flutter/lib/models/ab_model.dart:1007-1053.
- Auth: Bearer
- Headers:
Accept-Encoding: gzip - Response:
{
"data": "<stringified JSON of {tags,peers,tag_colors}>",
"licensed_devices": 100
}
data decoded:
{
"tags": ["tag1", ...],
"peers": [ { "id": "...", "alias": "...", ... }, ... ],
"tag_colors": "<stringified JSON map: { \"tag1\": -16776961, ... }>"
}
A response body of the literal string null (or empty) means "empty AB, no
error".
POST /api/ab
Source: flutter/lib/models/ab_model.dart:1055-1096.
- Auth: Bearer
- Body: the entire address book replaces what the server stores.
{ "data": "<stringified JSON of {tags,peers,tag_colors}>" }
Success: HTTP 200 with body empty / null / {}. Failure: {"error":"..."}.
5. Device groups, users, peers (group view)
These three endpoints together populate the "Group / Device" tab. All three are GET, paginated, Bearer-authenticated.
5.1 GET /api/device-group/accessible
Source: flutter/lib/models/group_model.dart:103-158.
- Query:
current,pageSize - Behaviour: the client treats any error from this endpoint as "old hbbs without device-group support" and silently continues.
Response page entry — DeviceGroupPayload:
{ "name": "Engineering" }
5.2 GET /api/users
Source: flutter/lib/models/group_model.dart:160-222.
- Query:
current,pageSize,accessible=(empty string),status=1 - Auth: Bearer
Response page entry — UserPayload (same shape as §3.1).
The client recognises the legacy errors "Admin required!" and
"ambiguous column name: status" and translates them to a "please upgrade
RustDesk Server Pro" toast. Backends should not produce them.
5.3 GET /api/peers
Source: flutter/lib/models/group_model.dart:224-282.
- Query:
current,pageSize,accessible=(empty),status=1 - Auth: Bearer
Response page entry — PeerPayload (from
flutter/lib/common/hbbs/hbbs.dart:77-131):
{
"id": "123456789",
"user": "alice",
"user_name": "Alice Doe",
"device_group_name": "Engineering",
"note": "string",
"status": 1,
"info": {
"username": "alice",
"device_name": "ALICE-PC",
"os": "Windows 10 / x64" // first " / "-separated token used
// to determine platform
}
}
6. Heartbeat & system info
These three endpoints are the "agent loop". They are never sent if the
configured API server matches the public rustdesk.com pattern
(src/hbbs_http/sync.rs:276-285).
The loop wakes every 3 s
(TIME_CONN in src/hbbs_http/sync.rs:19)
but only sends a heartbeat every 15 s unless connections changed
(TIME_HEARTBEAT in src/hbbs_http/sync.rs:17).
Sysinfo is re-uploaded at most every 120 s
(UPLOAD_SYSINFO_TIMEOUT in src/hbbs_http/sync.rs:18)
and only if hash/version differ.
6.1 POST /api/heartbeat
Source: src/hbbs_http/sync.rs:235-271.
- Auth: none
- Headers:
Content-Type: application/json
Request body:
{
"id": "123456789",
"uuid": "<base64 uuid>",
"ver": 123456, // numeric version (hbb_common::get_version_number)
"conns": [101, 102], // omitted if no active connections
"modified_at": 0 // last strategy timestamp the client knows
}
Response — any subset of:
{
"sysinfo": "any-truthy-value", // presence forces sysinfo re-upload
"disconnect": [101], // conn IDs the client should drop
"modified_at": 1700000000, // newer timestamp ⇒ persist locally
"strategy": {
"config_options": { "<key>": "<value>", ... },
"extra": { ... }
}
}
Any value the client does not recognise is ignored. strategy.config_options
is merged into the client's options; an empty value with no built-in default
removes the option, otherwise it overwrites.
6.2 POST /api/sysinfo
The client first probes versions with /api/sysinfo_ver (§6.3); if the version
matches what the server already has, this POST is skipped.
Source: src/hbbs_http/sync.rs:131-229.
- Auth: none
- Headers:
Content-Type: application/json
Request body (top-level fields are merged from
get_sysinfo() plus preset options):
{
"version": "1.4.x",
"id": "123456789",
"uuid": "<base64 uuid>",
"username": "alice", // OS username
"hostname": "ALICE-PC",
"os": "Windows 10 / x64",
"cpu": "...", "memory": "...", // and other fields from get_sysinfo
"preset_address_book_name": "...", // optional, only if configured
"preset_address_book_tag": "...",
"preset_address_book_alias": "...",
"preset_address_book_password": "...",
"preset_address_book_note": "...",
"preset_username": "...",
"preset_strategy_name": "...",
"preset_device_group_name": "..."
}
Response — body is treated as a bare string (not JSON):
| Body | Meaning |
|---|---|
SYSINFO_UPDATED |
Success. Client caches a SHA-256 of (URL+body) and the version returned by /api/sysinfo_ver. |
ID_NOT_FOUND |
Re-upload at next heartbeat tick (no cache). |
| anything else / err | Treated as success-with-deferral (cache still skipped). |
6.3 POST /api/sysinfo_ver
Source: src/hbbs_http/sync.rs:192-208.
- Auth: none
- Body: empty
- Response: an opaque version string. The client compares against its
cached
sysinfo_ver; if equal and the request hash is unchanged, the full sysinfo upload is skipped this cycle.
Backends without versioning may always return an empty string; the client will then upload sysinfo each cycle.
7. Audit logging
Audit endpoints are only called when the API server is non-public
(get_audit_server() in src/common.rs:1119-1125).
All three are POST with Content-Type: application/json, no Authorization
header, and fire-and-forget (response is ignored beyond logging). They share a
common envelope:
{
"id": "<peer id of this client>",
"uuid": "<base64 uuid of this client>",
...endpoint-specific fields
}
7.1 POST /api/audit/conn
Source: src/server/connection.rs:1248-1279.
{
"id": "...",
"uuid": "...",
"conn_id": 101,
"session_id": 7,
"ip": "192.0.2.10",
"action": "new"
}
Currently the only action emitted by the client is "new" (sent immediately
after the remote IP is verified against the IP whitelist). The server response
is ignored, but the dialog flow in §7.4 implies the server returns or stores
a guid per audit row.
7.2 POST /api/audit/file
Source: src/server/connection.rs:1297-1330.
{
"id": "...",
"uuid": "...",
"peer_id": "<remote peer id>",
"type": 0, // 0 = RemoteSend, 1 = RemoteReceive
"path": "C:\\path\\to\\dir",
"is_file": false,
"info": "<stringified JSON>"
}
info, decoded:
{
"ip": "192.0.2.10",
"name": "alice (display name)",
"num": 42, // total files in the operation
"files": [ { "name": "big.iso", "size": 4400000000 }, ... ] // top-10 by size
}
type enum from src/server/connection.rs:5063-5066:
0 = RemoteSend, 1 = RemoteReceive. is_file is true only when the
operation is a single file (files.len() == 1 && files[0].name == "").
7.3 POST /api/audit/alarm
Source: src/server/connection.rs:1332-1349.
{
"id": "...",
"uuid": "...",
"typ": 0, // see enum below
"info": "<stringified JSON, free-form>"
}
typ enum from src/server/connection.rs:5053-5061:
| Value | Constant | Meaning |
|---|---|---|
| 0 | IpWhitelist |
Connection rejected by IP whitelist. |
| 1 | ExceedThirtyAttempts |
>30 password attempts. |
| 2 | SixAttemptsWithinOneMinute |
6 password attempts in 60 s. |
| 6 | ExceedIPv6PrefixAttempts |
Per-/64 IPv6 attempt ceiling exceeded. |
(Values 3–5 are reserved / commented out.)
7.4 PUT /api/audit
Update an existing connection-audit row (used by the "leave a note at end of session" dialog).
- Source: flutter/lib/common/widgets/dialog.dart:1656-1687
- Auth: Bearer
- Headers:
Content-Type: application/json
{
"guid": "<audit-row guid as returned by /api/audit/conn>",
"note": "free-form text"
}
A 200 status is treated as success; non-200 is logged and discarded.
8. Session recording upload
Used when session-recording-on-the-server is enabled. All requests share the
endpoint POST /api/record and disambiguate via the type query parameter.
- Source: src/hbbs_http/record_upload.rs
- Auth: none
- Content-Type: the body is raw
application/octet-stream-style bytes (the client usesreqwest's default forBytes/Vec<u8>— no explicitContent-Typeheader). Servers should accept any. - Send cadence: at most every 1 s, or whenever ≥1 MiB of new data is
available, whichever comes first
(
SHOULD_SEND_TIMEandSHOULD_SEND_SIZEat lines 16-17).
Per-call query parameters:
type |
file |
offset |
length |
Body | When sent |
|---|---|---|---|---|---|
new |
yes | — | — | empty | New recording starts. |
part |
yes | byte offset (decimal) | bytes to follow | raw chunk | Periodic upload. |
tail |
yes | 0 |
header length (≤1024) | first ≤1024 bytes of file | Recording finished; uploaded after final part. |
remove |
yes | — | — | empty | Recording aborted. |
file is the basename, e.g. 2025-04-12_14-22-01.mp4.
Response: any JSON object. If it contains {"error": "..."}, the client
aborts the recording session and logs the error.
9. Generic file download
Used by the auto-update / installer-fetch path (src/hbbs_http/downloader.rs).
- The URL is arbitrary; not necessarily on the API server. Path-format agnostic.
- The client first sends
HEADto readContent-Length, thenGETto stream the body. Both calls go through the same TLS-fallback machinery as the API client. - The server MUST return
Content-Lengthon the HEAD response; without it the download is aborted with"Failed to get content length". - Streamed responses (chunked transfer-encoding) are fine for the GET.
10. Plugin signature service
For installations that ship signed plugins.
POST /lic/web/api/plugin-sign
- Source: src/plugin/callback_msg.rs:282-296
- Auth: none
- Headers:
Content-Type: application/json(set automatically by.json()) - Timeout: 10 s
Request body (PluginSignReq at
src/plugin/callback_msg.rs:82-87):
{
"plugin_id": "string",
"version": "string",
"msg": [/* byte array; serde will encode Vec<u8> as JSON array of u8 */]
}
Response (PluginSignResp):
{ "signed_msg": [/* byte array */] }
The bytes are passed verbatim into the plugin's
handle_signature_verification entry point.
11. CLI device assignment (--assign)
Triggered from a terminal: rustdesk --assign --token <bearer> [...]. Used by
mass-deploy scripts to register a freshly-installed agent into a tenant.
POST /api/devices/cli
- Source: src/core_main.rs:519-616
- Auth:
Authorization: Bearer <token>(passed via--token) - Headers:
Content-Type: application/json
Request body (only id/uuid are mandatory; at least one of the optional
fields must be present, see CLI help):
{
"id": "<this device's peer id>",
"uuid": "<base64 uuid>",
"user_name": "...", // optional
"strategy_name": "...", // optional
"address_book_name": "...", // optional
"address_book_tag": "...", // optional
"address_book_alias": "...", // optional
"address_book_password": "...", // optional
"address_book_note": "...", // optional
"device_group_name": "...", // optional
"note": "...", // optional
"device_username": "...", // optional
"device_name": "..." // optional
}
Response: plain text. Empty body ⇒ Done! is printed; otherwise the body
is printed verbatim to stdout. No structured error contract.
12. Endpoint index
| # | Method | Path | Auth | Notes |
|---|---|---|---|---|
| 1 | GET | /api/login-options |
none | Probe + SSO list |
| 2 | POST | /api/login |
none | Username/password, 2FA, SSO completion |
| 3 | POST | /api/currentUser |
Bearer | Refresh profile |
| 4 | POST | /api/logout |
Bearer | Best-effort |
| 5 | POST | /api/oidc/auth |
none | Begin device-flow |
| 6 | GET | /api/oidc/auth-query |
none | Poll device-flow |
| 7 | POST | /api/ab/settings |
Bearer | max_peer_one_ab |
| 8 | POST | /api/ab/personal |
Bearer | Personal AB GUID; 404 ⇒ legacy mode |
| 9 | POST | /api/ab/shared/profiles |
Bearer | Paginated AB list |
| 10 | POST | /api/ab/peers?ab=<guid> |
Bearer | Paginated peer list |
| 11 | POST | /api/ab/tags/{guid} |
Bearer | All tags |
| 12 | POST | /api/ab/peer/add/{guid} |
Bearer | Add one peer |
| 13 | PUT | /api/ab/peer/update/{guid} |
Bearer | Partial update of one peer |
| 14 | DELETE | /api/ab/peer/{guid} |
Bearer | Bulk delete by ID list |
| 15 | POST | /api/ab/tag/add/{guid} |
Bearer | Add one tag |
| 16 | PUT | /api/ab/tag/rename/{guid} |
Bearer | Rename |
| 17 | PUT | /api/ab/tag/update/{guid} |
Bearer | Set color |
| 18 | DELETE | /api/ab/tag/{guid} |
Bearer | Bulk delete by name list |
| 19 | GET | /api/ab |
Bearer | Legacy AB blob (gzip) |
| 20 | POST | /api/ab |
Bearer | Legacy AB blob save |
| 21 | GET | /api/device-group/accessible |
Bearer | Paginated; errors silently tolerated |
| 22 | GET | /api/users |
Bearer | Paginated, accessible=&status=1 |
| 23 | GET | /api/peers |
Bearer | Paginated, accessible=&status=1 |
| 24 | POST | /api/heartbeat |
none | Every 15 s (3 s when active) |
| 25 | POST | /api/sysinfo_ver |
none | Cache probe |
| 26 | POST | /api/sysinfo |
none | Bare-string response |
| 27 | POST | /api/audit/conn |
none | Connection start |
| 28 | POST | /api/audit/file |
none | File transfer summary |
| 29 | POST | /api/audit/alarm |
none | Security alarms |
| 30 | PUT | /api/audit |
Bearer | Update note on a conn audit row |
| 31 | POST | /api/record |
none | ?type=new|part|tail|remove&file=&offset=&length= |
| 32 | POST | /api/devices/cli |
Bearer | Used by rustdesk --assign |
| 33 | POST | /lic/web/api/plugin-sign |
none | Plugin signature |
| 34 | HEAD | (configured api-server) probe |
none | Performed once on startup against /api/login-options |
| 35 | HEAD+GET | (arbitrary URL) | none | Generic downloader; HEAD must return Content-Length |
13. Minimum viable backend
To stand up a backend that a stock RustDesk client can use end-to-end, in priority order:
- Connectivity probe — answer
HEAD /api/login-optionswith200 OK. - Auth core — implement
GET /api/login-options,POST /api/login,POST /api/logout,POST /api/currentUser. Returnaccess_tokenon successful login. - Heartbeat & sysinfo —
POST /api/heartbeat,POST /api/sysinfo,POST /api/sysinfo_ver. Without these the agent loop logs errors but continues; with them, the server has live device tracking. - Address book — pick one mode:
- Modern:
POST /api/ab/settings,POST /api/ab/personal,POST /api/ab/shared/profiles,POST /api/ab/peers,POST /api/ab/tags/{guid}, plus the per-peer / per-tag mutation set (§4.6, §4.7). - Legacy: respond
404to/api/ab/personal, then implementGET /api/ab+POST /api/ab(§4.8).
- Modern:
- Group view —
GET /api/users,GET /api/peers,GET /api/device-group/accessible. Without these the device-tab is empty. - Audit — implement the four audit routes (§7) only if you want server-side logging; the client tolerates 4xx/5xx silently.
- Optional:
/api/record,/api/devices/cli,/api/oidc/*,/lic/web/api/plugin-sign.
A backend that returns { "error": "..." } with a clear message on any
unsupported endpoint will produce reasonable UX in the client.