REST Endpoints
Every REST route on the Mosaic remote interface, grouped by resource, with request/response schemas.
This is the complete REST surface mounted by backend/remote.Mount on the /api prefix. Every route except POST /api/login and GET /api/bootstrap is gated by AuthGate. Every state-changing route except bearer-keyed callers is gated by OriginGuard.
On the mosaicd daemon the interface is multi-user: AuthGate resolves each request to an account and the response is scoped to it. Mutations are checked against that account’s role and permission flags — an unmet permission returns 403 {"error": "forbidden"}. The desktop build runs with a single implicit account and never returns 403.
DTO field names below are the JSON tags from backend/api; types are the Go-side shape. Where a DTO appears in multiple routes, its full schema is shown once at the top of its section.
CSRF carve-out reminder.
OriginGuardallows requests that have neither anOriginnor aRefererheader —SameSite=Stricton the session cookie means a real browser-mediated CSRF wouldn’t carry the cookie, and most non-browser clients omit both headers. Cookie-authed requests are not unconditionally Origin-checked. See Authentication for the full algorithm.
Authentication
POST /api/login
Public. Rate-limited per IP (5/min, burst 5). Issues a session cookie on success.
Request body
{ "username": "string", "password": "string" }
Responses
200{"ok": true}— setsmosaic_sessioncookie400body decode failure401{"error": "invalid credentials"}429{"error": "too many login attempts"}—Retry-After: 12is a constant (12 seconds) regardless of how many tokens are missing from the bucket; clients should treat it as “wait at least 12s and try once”.
curl -k -c cookies.txt -X POST https://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"hunter2"}'
POST /api/logout
Auth: session OR bearer. Origin guard applies.
Request body: none.
Response: 200 {"ok": true}. Deletes the server-side session and clears the cookie.
GET /api/bootstrap
Public (no auth). Lets the SPA learn which build it is talking to before login — the daemon shows the Users settings pane, the desktop build shows Web Interface.
Response: 200
{
flavor: "daemon" | "desktop";
version: string;
multi_user: boolean; // true for the daemon
}
Torrents
TorrentDTO shape
{
id: string; // info-hash
name: string;
magnet: string;
save_path: string;
total_bytes: number;
bytes_done: number;
progress: number; // 0..1
download_rate: number; // bytes/sec
upload_rate: number; // bytes/sec
peers: number;
seeds: number;
paused: boolean;
completed: boolean;
added_at: number; // unix seconds
category_id: number | null;
tags: TagDTO[];
queue_position: number;
force_start: boolean;
sequential: boolean; // earliest-first piece priority (see Sequential download)
queued: boolean; // capped by queue limits
verifying: boolean; // recheck in progress
files_missing: boolean; // on-disk pieces vanished between sessions
access: "owner" | "editor" | "viewer"; // the caller's access level
}
access is the requesting account’s access level on this torrent. Admins (and the desktop build) always see owner. It gates which mutations the caller may perform: viewer is read-only, editor may pause/resume/recheck and change priorities, owner may additionally delete and share.
GET /api/torrents
List all torrents. Stable sort: newest-first by added_at, tie-break by id.
Response: 200 [TorrentDTO, ...]
curl -k https://localhost:8080/api/torrents \
-H "Authorization: Bearer $MOSAIC_API_KEY"
POST /api/torrents/magnet
Add a torrent by magnet URI. Persists the magnet so it survives restart.
Request body
{ "magnet": "magnet:?xt=urn:btih:...", "save_path": "/optional/path" }
save_path is optional — empty defaults to GetDefaultSavePath().
Responses
200{"id": "<info-hash>"}400invalid magnet, engine refused, or persistence failed
curl -k -X POST https://localhost:8080/api/torrents/magnet \
-H "Authorization: Bearer $MOSAIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"magnet":"magnet:?xt=urn:btih:...&dn=...","save_path":""}'
POST /api/torrents/file
Add a torrent by uploading a .torrent file. multipart/form-data only; max 10 MiB body.
Form fields
file(file) — the.torrentblobsave_path(string, optional) — defaults to the global default
Responses
200{"id": "<info-hash>"}400parse / read / engine errors
curl -k -X POST https://localhost:8080/api/torrents/file \
-H "Authorization: Bearer $MOSAIC_API_KEY" \
-F "file=@./ubuntu.torrent" \
-F "save_path=/srv/downloads"
POST /api/torrents/{id}/pause
{id} is the info-hash. Response: 200 {"ok": true}.
POST /api/torrents/{id}/resume
Response: 200 {"ok": true}.
POST /api/torrents/{id}/recheck
Forces piece-by-piece reverification against the on-disk files.
Response: 200 {"ok": true}.
DELETE /api/torrents/{id}
Remove a torrent. Query param delete=1 also wipes the on-disk files; otherwise files are left in place.
Response: 200 {"ok": true}.
curl -k -X DELETE "https://localhost:8080/api/torrents/$INFOHASH?delete=1" \
-H "Authorization: Bearer $MOSAIC_API_KEY"
POST /api/torrents/category
Set or clear the category for a torrent.
Request body
{ "infohash": "string", "category_id": 7 }
category_id: null clears the category.
Response: 200 {"ok": true}.
POST /api/torrents/file_priorities
Set per-file piece priorities for a torrent.
Request body
{
"infohash": "string",
"priorities": { "0": "skip", "1": "normal", "2": "high", "3": "max" }
}
Keys are the file index (as string in JSON); values are one of "skip" | "normal" | "high" | "max". Any string outside that set is treated as "normal".
Response: 200 {"ok": true}.
POST /api/torrents/queue_position
{ "infohash": "string", "pos": 0 }
Lower pos means higher priority in the queue. Response: 200 {"ok": true}.
POST /api/torrents/force_start
{ "infohash": "string", "force": true }
force=true bypasses the queue limit for this torrent. Response: 200 {"ok": true}.
POST /api/torrents/sequential
Toggle sequential piece download (earliest-first request order) for a torrent. See Sequential download for what this does to the piece picker.
Request body
{ "infohash": "string", "enabled": true }
Response: 200 {"ok": true}. State persists in the torrents table and is re-applied on startup.
POST /api/torrents/{id}/trackers
Add a single tracker URL to an existing torrent. The new URL is announced to immediately. User-added trackers persist across restarts.
Request body
{ "url": "https://tracker.example.org/announce" }
Response: 200 {"ok": true}.
DELETE /api/torrents/{id}/trackers
Remove a tracker URL from a torrent. The URL is passed in the body, not the path, to avoid having to percent-encode ://. Anacrolix’s ModifyTrackers is used to atomically rebuild the announce list without the removed URL. Removal is persisted — even trackers from the original metainfo stay removed across restarts.
Request body
{ "url": "https://tracker.example.org/announce" }
Response: 200 {"ok": true}. No error if the URL isn’t present.
POST /api/torrents/url
Add a torrent by URL — either a magnet: URI or a direct https://…torrent link. The .torrent URL path issues a 10 MiB-capped fetch through the same SSRF-validated HTTP client the RSS poller uses, then passes the bytes to the engine. The magnet path is equivalent to POST /api/torrents/magnet.
Request body
{ "url": "magnet:?xt=urn:btih:… | https://example.org/file.torrent", "save_path": "/optional" }
Response: 200 {"ok": true} (note: this route doesn’t currently return the new torrent’s id; query GET /api/torrents if you need it).
Stats
GET /api/stats
Response: 200 GlobalStats
{
total_torrents: number;
active_torrents: number; // count of torrents where !Paused && !Completed
seeding_torrents: number; // count of torrents where Completed === true (regardless of Paused)
total_download_rate: number; // bytes/sec
total_upload_rate: number;
total_peers: number;
}
Inspector
The inspector is a per-client focus channel: tell the backend which torrent the user is looking at and which tabs are visible, and the inspector:tick WebSocket frame will scope to just that detail.
POST /api/inspector/focus
{
"id": "<info-hash>",
"tabs": ["overview", "files", "peers", "trackers", "speed"]
}
Empty id clears the focus. Tabs control which optional sections of DetailDTO get populated:
"files"populatesfiles[]"peers"populatespeers_list[]"trackers"populatestrackers[]"overview"and"speed"are accepted by the request body but are inert —scopeForTabsdoesn’t set any additional flags for them. Pass them or omit them; the resulting payload is the same.
Response: 200 {"ok": true}.
POST /api/inspector/clear
No body. Response: 200 {"ok": true}.
DetailDTO shape
{
id: string;
name: string;
magnet: string;
save_path: string;
total_bytes: number;
bytes_done: number;
progress: number;
ratio: number;
total_down: number; // lifetime bytes downloaded
total_up: number; // lifetime bytes uploaded
peers: number;
seeds: number;
added_at: number;
completed_at?: number; // unix seconds, omitted if not yet complete
paused: boolean;
completed: boolean;
files_missing: boolean;
files?: FileDTO[];
peers_list?: PeerDTO[];
trackers?: TrackerDTO[];
}
// FileDTO
{ index: number; path: string; size: number; bytes_done: number; progress: number; priority: "skip"|"normal"|"high"|"max" }
// PeerDTO
{ ip: string; port: number; client: string; flags: string; progress: number; download_rate: number; upload_rate: number; country: string }
// TrackerDTO
{ url: string; status: string; seeds: number; peers: number; downloaded: number; last_announce: number; next_announce: number }
DetailDTO is delivered through the inspector:tick WebSocket frame — there’s no REST GET for it. See WebSocket.
Categories
CategoryDTO
{ id: number; name: string; default_save_path: string; color: string }
GET /api/categories
Response: 200 [CategoryDTO, ...].
POST /api/categories
{ "name": "Movies", "default_save_path": "/media/movies", "color": "#7c3aed" }
Response: 200 {"id": 1}.
PUT /api/categories
{ "id": 1, "name": "Movies", "default_save_path": "/media/movies", "color": "#7c3aed" }
Response: 200 {"ok": true}.
DELETE /api/categories/{id}
Response: 200 {"ok": true}.
Tags
TagDTO
{ id: number; name: string; color: string }
GET /api/tags
Response: 200 [TagDTO, ...].
POST /api/tags
{ "name": "linux-iso", "color": "#67e8f9" }
Response: 200 {"id": 4}.
DELETE /api/tags/{id}
Response: 200 {"ok": true}.
POST /api/tags/assign
{ "infohash": "string", "tag_id": 4 }
Response: 200 {"ok": true}.
POST /api/tags/unassign
Same body shape as assign. Response: 200 {"ok": true}.
Settings: save path
GET /api/settings/save_path
Response: 200 {"path": "/Users/me/Downloads"}.
PUT /api/settings/save_path
{ "path": "/srv/downloads" }
Response: 200 {"ok": true}.
Settings: bandwidth limits
LimitsDTO
{
down_kbps: number;
up_kbps: number;
alt_down_kbps: number;
alt_up_kbps: number;
alt_active: boolean;
}
0 for either kbps field means no limit.
GET /api/settings/limits
Response: 200 LimitsDTO.
PUT /api/settings/limits
Body: LimitsDTO. Response: 200 {"ok": true}.
POST /api/settings/alt_speed/toggle
Flip the alt-speed override. Response: 200 {"alt_active": true} (the new state).
Settings: queue
QueueLimitsDTO
{ max_active_downloads: number; max_active_seeds: number }
0 means unlimited (the queue scheduler is bypassed).
GET /api/settings/queue_limits
Response: 200 QueueLimitsDTO.
PUT /api/settings/queue_limits
Body: QueueLimitsDTO. Response: 200 {"ok": true}.
Settings: peer
PeerLimitsDTO
{
listen_port: number; // 0 lets anacrolix pick at startup
max_peers_per_torrent: number; // 0 = anacrolix default (80)
dht_enabled: boolean;
encryption_enabled: boolean;
upnp_enabled: boolean; // automatic UPnP/NAT-PMP port forwarding
}
max_peers_per_torrent mutates at runtime; listen_port / dht_enabled / encryption_enabled / upnp_enabled take effect on next launch. upnp_enabled defaults to true (matching anacrolix’s own default) and toggles the NoDefaultPortForwarding flag on the underlying torrent.Client at startup.
GET /api/settings/peer_limits
Response: 200 PeerLimitsDTO.
PUT /api/settings/peer_limits
Body: PeerLimitsDTO. Response: 200 {"ok": true} or 400 if listen_port is outside 0..65535 or max_peers_per_torrent is negative.
Settings: seeding
Global default stop-conditions for seeding. Per-torrent overrides at GET/PUT /api/torrents/{id}/seed_policy. See Seed limits for the semantics.
SeedingDefaultsDTO
{
ratio_limit: number | null; // bytes_up / bytes_done; null = no limit
time_min_limit: number | null; // minutes since first completed-and-unpaused tick; null = no limit
}
GET /api/settings/seeding_defaults
Response: 200 SeedingDefaultsDTO.
PUT /api/settings/seeding_defaults
Body: SeedingDefaultsDTO. Response: 200 {"ok": true}.
SeedPolicyDTO
{
use_global: boolean; // true = clear override, fall back to global defaults
ratio_limit: number | null;
time_min_limit: number | null;
}
use_global: false with both *_limit fields null means “no limit, ignore globals” — distinct from use_global: true.
GET /api/torrents/{id}/seed_policy
Response: 200 SeedPolicyDTO.
PUT /api/torrents/{id}/seed_policy
Body: SeedPolicyDTO. Writing use_global: true deletes the per-torrent override row; writing the other shapes persists a small JSON blob on the torrents row. Response: 200 {"ok": true}.
A 30-second background ticker checks every completed, unpaused torrent against its effective policy (per-torrent beats global) and pauses any that have exceeded their ratio or time limit.
Per-torrent rate limits
Cap a single torrent’s download/upload speed independent of the global cap. The effective speed is the minimum of all applicable caps. See Per-Torrent Rate Limits for behavior notes (duty-cycle limiter, coarse-grained at low caps).
TorrentRateLimitsDTO
{
down_kbps: number; // KB/s, 0 = unlimited
up_kbps: number; // KB/s, 0 = unlimited
}
GET /api/torrents/{id}/rate_limits
Response: 200 TorrentRateLimitsDTO. Requires viewer-or-higher access on the torrent.
PUT /api/torrents/{id}/rate_limits
Body: TorrentRateLimitsDTO. Persists to the torrents row and re-applies to the live engine; the duty-cycle limiter goroutine starts on first non-zero set and exits when both axes are 0 again. Requires editor-or-owner access. Response: 200 {"ok": true} or 400 if any field is negative.
Settings: watch folder
Auto-add .torrent files dropped into a directory. See Watch Folder for the polling cadence + dedup story.
WatchFolderDTO
{
path: string;
delete_after_add: boolean;
enabled: boolean;
}
GET /api/settings/watch_folder
Response: 200 WatchFolderDTO.
PUT /api/settings/watch_folder
Body: WatchFolderDTO. Persists to settings and restarts the polling goroutine — switching enabled, swapping path, or toggling delete_after_add all take effect on the next poll without a restart. Response: 200 {"ok": true}.
Note: a native directory-picker dialog (PickWatchFolder in the Wails binding) is desktop-only — there’s no remote equivalent for popping an OS dialog on the user’s machine.
Settings: blocklist
BlocklistDTO
{
url: string;
enabled: boolean;
last_loaded_at: number; // unix seconds, 0 if never
entries: number; // newline count of last fetch
error?: string;
}
GET /api/settings/blocklist
Response: 200 BlocklistDTO.
PUT /api/settings/blocklist
{ "url": "https://example.com/blocklist.p2p", "enabled": true }
url is validated against the SSRF allow-list (http/https only, no private/loopback addresses). Disabling clears the in-memory blocklist state and tells the engine to drop its filter. Enabling kicks off an immediate fetch.
Response: 200 {"ok": true} or 400 with the validation error.
POST /api/settings/blocklist/refresh
Re-fetch the configured blocklist URL. Body cap at 50 MiB; 30s timeout.
Responses
200 {"ok": true}on success400if no URL is configured / URL re-validation failed502on upstream failure
Users & accounts
Multi-user account management. Meaningful on the mosaicd daemon; on the desktop build there is a single implicit account. All routes require an authenticated session. The admin-only routes return 403 for non-admin callers.
UserDTO
{
id: number;
username: string;
role: "admin" | "user";
perm_add_torrents: boolean;
perm_manage_rss: boolean;
perm_manage_cat_tags: boolean;
perm_change_settings: boolean;
perm_share: boolean;
has_api_key: boolean;
api_key_hint: string; // last 4 chars, for display
password_set: boolean;
disabled: boolean;
created_at: number; // unix seconds
}
GET /api/me
The calling account’s own UserDTO.
POST /api/me/password
Body { "old_password": "...", "new_password": "..." }. Confirms the current password, sets the new one (minimum 8 characters), and revokes the caller’s sessions. Response: 200 {"ok": true}.
POST /api/me/api_key/rotate
Mints a fresh API key for the calling account. Response: 200 {"api_key": "<token>"} — shown once; only a SHA-256 hash and a 4-char hint are stored.
GET /api/users
Admin only. Response: 200 UserDTO[].
POST /api/users
Admin only. Creates an account.
{
username: string;
password: string; // required, minimum 8 characters
role: "admin" | "user";
perm_add_torrents: boolean;
perm_manage_rss: boolean;
perm_manage_cat_tags: boolean;
perm_change_settings: boolean;
perm_share: boolean;
disabled: boolean;
}
role: "admin" forces every permission on. Response: 200 UserDTO.
PUT /api/users/{id}
Admin only. Same body as create; password is ignored (use the reset route). Refuses any change that would leave zero enabled admins. Response: 200 UserDTO.
DELETE /api/users/{id}
Admin only. The primary admin (id 1) and your own account cannot be deleted. Response: 200 {"ok": true}.
POST /api/users/{id}/password
Admin only. Body { "new_password": "..." } — resets another account’s password and revokes its sessions. Response: 200 {"ok": true}.
Torrent sharing
A torrent is owned by whoever added it; an owner can share it with other accounts at viewer (read-only) or editor (control, no delete/re-share) level. All routes require owner access on the torrent.
ShareDTO
{
user_id: number;
username: string;
access: "owner" | "editor" | "viewer";
}
GET /api/torrents/{id}/shares
Every access grant on the torrent. Response: 200 ShareDTO[].
POST /api/torrents/{id}/shares
Body { "user_id": number, "access": "viewer" | "editor" }. Grants or updates another account’s access. owner cannot be granted this way. Response: 200 {"ok": true}.
DELETE /api/torrents/{id}/shares/{userID}
Revokes an account’s access. An owner grant cannot be revoked here. Response: 200 {"ok": true}.
Settings: web interface
WebConfigDTO
{
enabled: boolean;
port: number;
bind_all: boolean;
username: string;
api_key: string;
}
This route configures the optional embedded web server. The username/api_key fields map to the admin account (id 1): username is its login name, and api_key here is the non-secret last-4 hint of its key, not the key itself. On mosaicd, server config and accounts are managed from Settings → Users instead.
GET /api/settings/web
Response: 200 WebConfigDTO.
PUT /api/settings/web
Body: WebConfigDTO. The server only persists enabled, port, bind_all, and username — api_key is ignored (rotate via the dedicated route). Changing username renames the admin account and revokes its sessions so pre-change cookies stop working. Restarts the underlying remote.Server if the bind/port/enabled state changed.
Response: 200 {"ok": true}.
PUT /api/settings/web/password
{ "password": "<new-plaintext>" }
Sets the admin account’s password (Argon2id), marks it operator-set, and revokes that account’s sessions. Response: 200 {"ok": true}. Regular accounts change their own password via POST /api/me/password.
POST /api/settings/web/api_key/rotate
Mints a fresh API key for the admin account and returns it once — only a SHA-256 hash is stored. Equivalent to POST /api/me/api_key/rotate when called as the admin.
Response: 200 {"api_key": "<token>"}. The token is base64.RawURLEncoding-encoded (URL-safe alphabet, no padding) — characters are drawn from [A-Za-z0-9-_].
Settings: updater
UpdaterConfigDTO
{
enabled: boolean;
channel: "stable" | "beta";
last_checked_at: number; // unix seconds
last_seen_version: string; // last "available" version observed
install_source: "apt" | "appimage" | "manual"; // how the running binary got onto disk
}
install_source is detected at startup. When it’s "apt" the in-app updater goes dormant — the periodic check is skipped, the install button refuses, and the Settings → Updates pane renders a Managed by apt banner. AppImage and manual installs use the in-app updater normally.
GET /api/settings/updater
Response: 200 UpdaterConfigDTO.
PUT /api/settings/updater
Body: UpdaterConfigDTO (server only persists enabled + channel). Validates channel is "stable" or "beta".
Response: 200 {"ok": true} or 400 {"error": "channel must be stable or beta"}.
POST /api/updater/check
Forces a release check and returns the result.
Response: 200 UpdateInfoDTO
{
available: boolean;
latest_version: string;
asset_url: string;
asset_filename: string;
checked_at: number; // unix seconds
current_version: string; // build-time version
}
500 with {"error":"updater disabled"} if AttachUpdater was never called (test/headless builds).
POST /api/updater/install
Runs the install with SHA-256 manifest verification. On success, fires the desktop “update installed” notification (if a Notifier is attached and the user hasn’t disabled it).
Response: 200 {"ok": true} on success; 500 on failure.
GET /api/version
Response: 200 {"version": "v1.5.0"}. The build-time version string.
Schedule rules
Time-of-day bandwidth rules. When any rule’s window overlaps now, its limits override the global LimitsDTO. When multiple enabled rules match the current time, the engine picks one — the current implementation defers to the rule query order from SQLite. Treat the conflict-resolution behavior as undefined and avoid configuring overlapping rules.
ScheduleRuleDTO
{
id: number;
days_mask: number; // bit 0 = Sunday, bit 6 = Saturday; 127 = all days
start_min: number; // minutes since midnight, 0..1439
end_min: number; // minutes since midnight; if < start_min, wraps midnight
down_kbps: number;
up_kbps: number;
alt_only: boolean; // when true, just enables alt-speed instead of overriding
enabled: boolean;
}
GET /api/schedule_rules
Response: 200 [ScheduleRuleDTO, ...].
POST /api/schedule_rules
Body: ScheduleRuleDTO (id is ignored). Response: 200 {"id": <int>}.
PUT /api/schedule_rules
Body: ScheduleRuleDTO. Response: 200 {"ok": true}.
DELETE /api/schedule_rules/{id}
Response: 200 {"ok": true}.
RSS feeds
FeedDTO
{
id: number;
url: string; // http(s); SSRF-validated
name: string;
interval_min: number; // minutes between polls; 0 → service default
last_polled: number; // unix seconds, 0 if never
etag: string; // last ETag seen, used as If-None-Match
enabled: boolean;
}
GET /api/feeds
Response: 200 [FeedDTO, ...].
POST /api/feeds
Body: FeedDTO (id ignored). URL validated through validateFetchURL.
Response: 200 {"id": <int>}, or 400 with the validation error.
PUT /api/feeds
Body: FeedDTO. Same URL validation as create.
Response: 200 {"ok": true} or 400.
DELETE /api/feeds/{id}
Response: 200 {"ok": true}.
GET /api/feeds/{feedID}/items
Live-fetch the feed (bypassing the per-feed dedup cache) and return every item with its resolved torrent source. Used by the SPA’s expandable feed-item browser.
Response: 200 [FeedItemDTO, ...]
// FeedItemDTO
{
guid: string;
title: string;
pub_date: string; // "YYYY-MM-DD" in UTC, empty if missing
torrent_url: string; // magnet: URI if present, else https://…torrent, else ""
}
POST /api/torrents/url
Add a torrent from a URL. Accepts both magnet: URIs and direct .torrent URLs (10 MiB-capped fetch). Used by the feed-item Add button — see also the Torrents → POST /api/torrents/url entry above.
RSS filters
Filters live under a feed and gate which items get auto-added.
FilterDTO
{
id: number;
feed_id: number;
regex: string; // POSIX/Go regexp; empty = match all
category_id: number | null;
save_path: string; // empty = inherit category default → global default
enabled: boolean;
}
GET /api/feeds/{feedID}/filters
Response: 200 [FilterDTO, ...].
POST /api/filters
Body: FilterDTO (id ignored). Response: 200 {"id": <int>}.
PUT /api/filters
Body: FilterDTO. Response: 200 {"ok": true}.
DELETE /api/filters/{id}
Response: 200 {"ok": true}.
WebSocket
A single upgrade route, not a REST endpoint:
GET /api/ws
Auth: session cookie OR Authorization: Bearer <api-key> header. The legacy ?key=<api-key> query-param form was removed because URL params leak into Referer headers, reverse-proxy access logs, and browser history. Origin pinned to Host.
The server pushes Envelope-shaped JSON frames at ~1 Hz; clients send nothing (reads are drained for liveness only).
See WebSocket for the full frame catalog.