Files
rustdesk/docs/CONSOLE_API.md
mike 47f0d0fff2
build-linux / build-linux-x64 (push) Successful in 5m23s
build-macos / build-macos-x64 (push) Successful in 9m4s
build-windows / build-windows-x64 (push) Successful in 10m13s
Implement CI workflow for Gitea. Include provision scripts for Gitea runners.
2026-05-07 09:39:23 +02:00

1039 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <access_token>
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": "<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:
```json
{
"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](../src/hbbs_http/account.rs#L31-L44)):
```json
{
"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](../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/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/<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](../flutter/lib/common/hbbs/hbbs.dart#L11-L15)):
`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](../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/<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 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": "<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](../flutter/lib/models/user_model.dart#L155-L175)
- **Auth:** Bearer
- **Timeout:** 2 s; failures are silently ignored client-side.
**Request body:**
```json
{ "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](../src/hbbs_http/account.rs#L160-L176)
- **Auth:** none
**Request body:**
```json
{
"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](../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": "<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:
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=<guid>`
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=<guid>`
- **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": "<stringified JSON of {tags,peers,tag_colors}>",
"licensed_devices": 100
}
```
`data` decoded:
```json
{
"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](../flutter/lib/models/ab_model.dart#L1055-L1096).
- **Auth:** Bearer
- **Body:** the entire address book replaces what the server stores.
```json
{ "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](../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": "<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:
```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": { "<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](../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": "<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](../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": "<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](../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": "<remote peer id>",
"type": 0, // 0 = RemoteSend, 1 = RemoteReceive
"path": "C:\\path\\to\\dir",
"is_file": false,
"info": "<stringified JSON>"
}
```
`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": "<stringified JSON, free-form>"
}
```
`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 35 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": "<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](../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<u8>` — 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<u8> 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 <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](../src/core_main.rs#L519-L616)
- **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):
```json
{
"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:
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.