# 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](../src/common.rs#L1088)). 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-tls` first, then `native-tls`, then with `danger_accept_invalid_certs`) on the first call and caches the result. See [src/hbbs_http/http_client.rs](../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. See [`HbbHttpResponse::parse` in src/hbbs_http.rs:24-39](../src/hbbs_http.rs#L24-L39): ```json { "error": "human readable message" } ``` - Default request timeout: **12 seconds** ([src/common.rs:1431](../src/common.rs#L1431)). - Logout has a hard **2 second** timeout ([flutter/lib/models/user_model.dart:168](../flutter/lib/models/user_model.dart#L168)). - The plugin-sign call uses **10 seconds** ([src/plugin/callback_msg.rs:293](../src/plugin/callback_msg.rs#L293)). ### 1.2 Headers For every authenticated request the client adds: ``` Authorization: Bearer Content-Type: application/json ``` `Authorization` is built by [`getHttpHeaders()` in flutter/lib/common.dart:2691-2695](../flutter/lib/common.dart#L2691-L2695) 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": "" }` 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=&pageSize= ``` The client always uses `pageSize=100` and iterates pages until `current * pageSize >= total`. Response: ```json { "total": , "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](../src/hbbs_http/account.rs#L31-L44)): ```json { "os": "Linux | Windows | macOS | Android | iOS", "type": "client | browser", "name": "" } ``` --- ## 2. Base URL & API discovery The client resolves its base URL via [`get_api_server()` in src/common.rs](../src/common.rs); a custom-rendezvous server with no explicit `api-server` is mapped to `https://` (port 21114 if specified inline). The client makes a `HEAD` request against `/api/login-options` on startup as a connectivity / TLS-detection probe ([src/hbbs_http/account.rs:155](../src/hbbs_http/account.rs#L155), [src/hbbs_http/record_upload.rs:34](../src/hbbs_http/record_upload.rs#L34)). 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](../flutter/lib/models/user_model.dart#L178-L202) - **Auth:** none - **Headers:** `Content-Type: application/json` (sent implicitly by `http.post`) **Request body** (from `LoginRequest.toJson()` at [flutter/lib/common/hbbs/hbbs.dart:133-178](../flutter/lib/common/hbbs/hbbs.dart#L133-L178)): ```json { "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/", "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](../flutter/lib/common/hbbs/hbbs.dart#L11-L15)): `account`, `mobile`, `sms_code`, `email_code`, `tfa_code`. `oidc/` 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](../flutter/lib/common/hbbs/hbbs.dart#L180-L197)): ```json { "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](../flutter/lib/common/hbbs/hbbs.dart#L17-L19)): | `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](../flutter/lib/models/user_model.dart#L222-L245) - **Auth:** none - **Method:** `GET` (no body) **Response** — JSON array of strings: ```json [ "account", "oidc/google", "oidc/github", "common-oidc/[ {\"name\":\"google\",\"icon\":\"...\"} ]" ] ``` Two recognised conventions: - `oidc/` — exposed in the UI as an SSO button labelled ``. - `common-oidc/` — the suffix is a JSON-encoded array of provider descriptors `{ "name": "...", ... }`. If present anywhere in the list, it takes precedence over individual `oidc/...` 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](../flutter/lib/models/user_model.dart#L60-L99) - **Auth:** Bearer **Request body:** ```json { "id": "", "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](../flutter/lib/models/user_model.dart#L155-L175) - **Auth:** Bearer - **Timeout:** 2 s; failures are silently ignored client-side. **Request body:** ```json { "id": "", "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](../src/hbbs_http/account.rs#L160-L176) - **Auth:** none **Request body:** ```json { "op": "", // matches an entry from /api/login-options "id": "", "uuid": "", "deviceInfo": { "os": "...", "type": "...", "name": "..." } } ``` **Response** — `OidcAuthUrl` ([src/hbbs_http/account.rs:24-28](../src/hbbs_http/account.rs#L24-L28)): ```json { "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](../src/hbbs_http/account.rs#L178-L202) - **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: ```json { "body": "" } ``` 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: 1. Clear `access_token` and cached `user_info` from local config. 2. 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](../flutter/lib/models/user_model.dart#L81-L83)). --- ## 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/personal` to signal legacy mode ([flutter/lib/models/ab_model.dart:271-274](../flutter/lib/models/ab_model.dart#L271-L274)). - **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](../flutter/lib/models/ab_model.dart#L230-L258) - **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 surfaces `pull_ab_failed_tip`. **Response:** ```json { "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](../flutter/lib/models/ab_model.dart#L262-L293) - **Auth:** Bearer - **Request body:** empty - **Special status:** `404` ⇒ legacy mode; client falls back to §4.7/§4.8. **Response:** ```json { "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](../flutter/lib/models/ab_model.dart#L295-L360) - **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`:** ```json { "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](../flutter/lib/common/hbbs/hbbs.dart#L210-L256). ### 4.4 `POST /api/ab/peers?ab=` Paginated list of peers in a given address book. - **Source:** [flutter/lib/models/ab_model.dart:1432-1497](../flutter/lib/models/ab_model.dart#L1432-L1497) - **Auth:** Bearer - **Method:** `POST` - **Query:** `current`, `pageSize`, `ab=` - **Request body:** empty **Response page entry — `Peer`:** ```json { "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](../flutter/lib/models/ab_model.dart#L1561-L1565)). ### 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](../flutter/lib/models/ab_model.dart#L1499-L1544) - **Auth:** Bearer - **Method:** `POST` - **Request body:** empty **Response:** ```json [ { "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](../flutter/lib/models/ab_model.dart#L1548-L1578). ```json { "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](../flutter/lib/models/ab_model.dart#L1580-L1729). 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. ```json { "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](../flutter/lib/models/ab_model.dart#L1751-L1771). Body is a JSON array of peer IDs: ```json ["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](../flutter/lib/models/ab_model.dart#L1775-L1802). The client iterates one POST per tag. ```json { "name": "tag", "color": -16776961 } ``` #### `PUT /api/ab/tag/rename/{guid}` Source: [flutter/lib/models/ab_model.dart:1804-1831](../flutter/lib/models/ab_model.dart#L1804-L1831). ```json { "old": "old-name", "new": "new-name" } ``` #### `PUT /api/ab/tag/update/{guid}` — set color Source: [flutter/lib/models/ab_model.dart:1833-1855](../flutter/lib/models/ab_model.dart#L1833-L1855). ```json { "name": "tag", "color": -16776961 } ``` #### `DELETE /api/ab/tag/{guid}` — bulk delete Source: [flutter/lib/models/ab_model.dart:1857-1876](../flutter/lib/models/ab_model.dart#L1857-L1876). 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](../flutter/lib/models/ab_model.dart#L1007-L1053). - **Auth:** Bearer - **Headers:** `Accept-Encoding: gzip` - **Response:** ```json { "data": "", "licensed_devices": 100 } ``` `data` decoded: ```json { "tags": ["tag1", ...], "peers": [ { "id": "...", "alias": "...", ... }, ... ], "tag_colors": "" } ``` 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](../flutter/lib/models/ab_model.dart#L1055-L1096). - **Auth:** Bearer - **Body:** the entire address book replaces what the server stores. ```json { "data": "" } ``` 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](../flutter/lib/models/group_model.dart#L103-L158). - **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`:** ```json { "name": "Engineering" } ``` ### 5.2 `GET /api/users` Source: [flutter/lib/models/group_model.dart:160-222](../flutter/lib/models/group_model.dart#L160-L222). - **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](../flutter/lib/models/group_model.dart#L224-L282). - **Query:** `current`, `pageSize`, `accessible=` (empty), `status=1` - **Auth:** Bearer **Response page entry — `PeerPayload`** (from [flutter/lib/common/hbbs/hbbs.dart:77-131](../flutter/lib/common/hbbs/hbbs.dart#L77-L131)): ```json { "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](../src/hbbs_http/sync.rs#L276-L285)). The loop wakes every 3 s ([`TIME_CONN` in src/hbbs_http/sync.rs:19](../src/hbbs_http/sync.rs#L19)) but only sends a heartbeat every **15 s** unless connections changed ([`TIME_HEARTBEAT` in src/hbbs_http/sync.rs:17](../src/hbbs_http/sync.rs#L17)). Sysinfo is re-uploaded at most every **120 s** ([`UPLOAD_SYSINFO_TIMEOUT` in src/hbbs_http/sync.rs:18](../src/hbbs_http/sync.rs#L18)) and only if hash/version differ. ### 6.1 `POST /api/heartbeat` Source: [src/hbbs_http/sync.rs:235-271](../src/hbbs_http/sync.rs#L235-L271). - **Auth:** none - **Headers:** `Content-Type: application/json` **Request body:** ```json { "id": "123456789", "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: ```json { "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": { "": "", ... }, "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](../src/hbbs_http/sync.rs#L131-L229). - **Auth:** none - **Headers:** `Content-Type: application/json` **Request body** (top-level fields are merged from [`get_sysinfo()`](../src/common.rs) plus preset options): ```json { "version": "1.4.x", "id": "123456789", "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](../src/hbbs_http/sync.rs#L192-L208). - **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](../src/common.rs#L1119-L1125)). 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: ```json { "id": "", "uuid": "", ...endpoint-specific fields } ``` ### 7.1 `POST /api/audit/conn` Source: [src/server/connection.rs:1248-1279](../src/server/connection.rs#L1248-L1279). ```json { "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](../src/server/connection.rs#L1297-L1330). ```json { "id": "...", "uuid": "...", "peer_id": "", "type": 0, // 0 = RemoteSend, 1 = RemoteReceive "path": "C:\\path\\to\\dir", "is_file": false, "info": "" } ``` `info`, decoded: ```json { "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](../src/server/connection.rs#L5063-L5066): `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](../src/server/connection.rs#L1332-L1349). ```json { "id": "...", "uuid": "...", "typ": 0, // see enum below "info": "" } ``` `typ` enum from [src/server/connection.rs:5053-5061](../src/server/connection.rs#L5053-L5061): | 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](../flutter/lib/common/widgets/dialog.dart#L1656-L1687) - **Auth:** Bearer - **Headers:** `Content-Type: application/json` ```json { "guid": "", "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](../src/hbbs_http/record_upload.rs) - **Auth:** none - **Content-Type:** the body is raw `application/octet-stream`-style bytes (the client uses `reqwest`'s default for `Bytes`/`Vec` — no explicit `Content-Type` header). 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_TIME` and `SHOULD_SEND_SIZE` at lines 16-17](../src/hbbs_http/record_upload.rs#L16-L17)). **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](../src/hbbs_http/downloader.rs)). - The URL is **arbitrary**; not necessarily on the API server. Path-format agnostic. - The client first sends `HEAD` to read `Content-Length`, then `GET` to stream the body. Both calls go through the same TLS-fallback machinery as the API client. - The server **MUST** return `Content-Length` on 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](../src/plugin/callback_msg.rs#L282-L296) - **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](../src/plugin/callback_msg.rs#L82-L87)): ```json { "plugin_id": "string", "version": "string", "msg": [/* byte array; serde will encode Vec as JSON array of u8 */] } ``` **Response** (`PluginSignResp`): ```json { "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 [...]`. 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](../src/core_main.rs#L519-L616) - **Auth:** `Authorization: Bearer ` (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): ```json { "id": "", "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=` | 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: 1. **Connectivity probe** — answer `HEAD /api/login-options` with `200 OK`. 2. **Auth core** — implement `GET /api/login-options`, `POST /api/login`, `POST /api/logout`, `POST /api/currentUser`. Return `access_token` on successful login. 3. **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. 4. **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 `404` to `/api/ab/personal`, then implement `GET /api/ab` + `POST /api/ab` (§4.8). 5. **Group view** — `GET /api/users`, `GET /api/peers`, `GET /api/device-group/accessible`. Without these the device-tab is empty. 6. **Audit** — implement the four audit routes (§7) only if you want server-side logging; the client tolerates 4xx/5xx silently. 7. **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.