On this page
Developer & APIREST API

REST API Reference

Comprehensive REST API documentation for host administration, scoped API tokens, telemetry, and self-updater.

9 min readUpdated August 2026

Authentication & Scoped Tokens#

All API endpoints require authentication using Basic Auth (admin credentials) or Scoped Bearer Tokens via the Authorization: Bearer <token> header.

State-changing requests (POST, DELETE) from browser clients validate CSRF tokens via X-CSRF-Token. Non-browser API clients (curl, scripts, Home Assistant) are exempt from CSRF checks.

Scoped API Tokens Endpoints#

Mint, list, and revoke automation tokens. The plaintext token is returned only on create.

GET/api/tokens
Auth: Admin or tokens:manage

List active automation tokens with assigned scopes (hashes omitted).

Scopes: tokens:manage

Response Example

json
{
  "status": true,
  "status_code": 200,
  "tokens": [
    {
      "name": "home-assistant",
      "scopes": ["stream:control", "logs:get"]
    }
  ]
}

Stream Telemetry & Adaptive Bitrate#

Host-side latency, bitrate bounds, client network feedback, and error counters.

GET/api/stream/latency
Auth: logs:get

Host-side latency breakdown in milliseconds.

Response Example

json
{
  "status": true,
  "capture_ms": { "min": 0.8, "max": 2.1, "avg": 1.1, "samples": 300 },
  "encode_ms": { "min": 1.2, "max": 3.4, "avg": 1.8, "samples": 300 }
}

Host Self-Updater Endpoints#

Linux in-app updater. Active streams block apply until idle unless forced.

GET/api/update
Auth: config:get

Query updater state, progress, and release notes.

Game Scanner & Health Check#

GET/api/games/scan
Auth: apps:get scope

Scan host for Steam, Lutris, and Heroic installed games.

Response Example

json
[
  {
    "name": "Hades",
    "path": "/home/user/.steam/steam/steamapps/common/Hades/Hades",
    "launcher": "steam"
  }
]
GET/api/health
Auth: Unauthenticated

Health check endpoint for container orchestrators and load balancers.

Response Example

json
{
  "status": "ok",
  "status_code": 200,
  "version": "2026.909.1",
  "uptime": 3600
}

Transport, auth & envelopes#

Base URL is https://<host>:47990/api/... — the Web UI port is the GameStream port (default 47989) plus one, and port is configurable. TLS uses the configured cert/pkey pair (self-signed by default; pass -k/--insecure to curl or install a trusted cert). Every JSON response carries Strict-Transport-Security, X-Frame-Options: DENY, and frame-ancestors none. Success envelope is {status: true, status_code: 200, ...}; failures are {status: false, status_code, error} with 400 validation, 401 unauthenticated (+ WWW-Authenticate: Basic realm="Sunshine Gamestream Host"), 403 wrong scope / origin / CSRF, 404 unknown path, 413 bodies over 1 MiB, 429 rate-limited. Auth tries Authorization: Bearer <64-hex-token> first (SHA-256 of plaintext:salt vs api_tokens entries), then HTTP Basic (always full admin, bypasses scopes). A * token is admin. Full scope vocabulary: config:get, config:set, apps:get, apps:launch, apps:close, clients:list, clients:pair, clients:unpair, logs:get, display:reset, tokens:manage, *. Login failures are bucketed per IP (10 fails / 30 s → 429); success resets. Mutating browser calls need X-CSRF-Token (or ?csrf_token=); mint one via GET /api/csrf-token (1 h TTL, per client id). Same-origin and origin-allowlisted (csrf_allowed_origins plus auto localhost) calls skip the token; curl/scripts with no Origin/Referer are exempt.

bash
curl -ku admin:pass https://localhost:47990/api/config | jq .version
TOK=sf_hex_here
curl -sk -H "Authorization: Bearer $TOK" https://localhost:47990/api/sessions?limit=5 | jq .
CSRF=$(curl -sk -u admin:pass https://localhost:47990/api/csrf-token | jq -r .csrf_token)
curl -sk -u admin:pass -H "X-CSRF-Token: $CSRF" -H 'Content-Type: application/json' -d '{}' https://localhost:47990/api/config -w '%{http_code}\n'

Apps, covers, browser & game scanner#

GET/api/apps
Auth: apps:get

Return the apps.json catalog as {apps[]}. An empty file yields {apps:[]}.

POST/api/apps
Auth: config:set + CSRF, Content-Type: application/json

Create (index -1) or update (index N) an app: {name, output, cmd, index, exclude-global-prep-cmd, elevated, auto-detach, wait-all, exit-timeout, prep-cmd[{do, undo, elevated}], detached[], image-path}.

DELETE/api/apps/{index}
Auth: config:set + CSRF

Delete the app at numeric index. Path regex only matches digits.

POST/api/apps/close
Auth: apps:close + CSRF

Terminate the running streamed process.

GET/api/covers/{index}
Auth: apps:get

Return image/png artwork for the app index. 404 when none is set.

POST/api/covers/upload
Auth: config:set

Upload artwork as {key, url|data}. Key must not contain /../ or NUL; url host must be images.igdb.com, otherwise send base64 data. Saved to appdata/covers/<key>.png.

GET/api/browse
Auth: config:get

Browse host paths: ?path=&type=directory|executable|file|any returns {path, parent, entries[{name, path, type}]}. Empty path lists / (Linux) or drives (Windows). Combine with least-privilege tokens and origin_web_ui_allowed to limit exposure.

Clients & pairing#

GET/api/clients/list
Auth: clients:list

List paired clients as {status, named_certs[]}.

POST/api/clients/unpair
Auth: clients:unpair + CSRF + JSON

Unpair one client: {uuid}.

POST/api/clients/unpair-all
Auth: clients:unpair + CSRF

Erase all pairings and terminate running sessions.

POST/api/clients/update
Auth: admin + CSRF + JSON

Enable or disable a cert: {uuid, enabled}. Disabling kills that client sessions.

POST/api/pin
Auth: clients:pair + CSRF + JSON

Pair with the Moonlight PIN: {pin: 0000-9999, name}. Returns {status}.

POST/api/password
Auth: admin + CSRF + JSON (unauth on first run)

Set credentials: {currentUsername, currentPassword, newUsername, newPassword, confirmNewPassword}. First-run with no username set is unauthenticated and redirects to /welcome otherwise.

Config, logs & system#

GET/api/config
Auth: config:get

Dump effective config: {status, platform, version, ...sunshine.conf vars plus solarflare audio_fx/headless/adaptive/trusted defaults}.

POST/api/config
Auth: config:set + CSRF, JSON object only

Write {key: value} pairs. Arrays, scalars, or empty objects are rejected with 400 (they would wipe the file); write failures return {status: false, error}.

GET/api/configLocale
Auth: Unauthenticated

Return {status, locale} for the Web UI boot path.

GET/api/logs
Auth: logs:get

Return text/plain host logs (not JSON).

GET/api/csrf-token
Auth: Any authenticated caller

Mint a CSRF token: {csrf_token} (32 random bytes, 1 h TTL).

GET/api/stream/telemetry
Auth: logs:get

Host resource monitor: {status, status_code, telemetry{host_cpu_pct[], host_gpu_pct[], host_ram_used_mb[], window_s}}. Non-Linux returns window_s only.

GET/api/sessions
Auth: logs:get

Session history with ?limit=100&app=&client=. Records match webhook payloads minus the event wrapper.

POST/api/reset-display-device-persistence
Auth: display:reset + CSRF

Reset Windows display-device persistence. Windows-only.

POST/api/restart
Auth: admin + CSRF

Restart the host process. May not return a body.

GET/api/vigembus/status
Auth: config:get

ViGEmBus driver state: {installed, version, version_compatible, packaged_version}. Non-Windows returns error with empty values.

POST/api/vigembus/install
Auth: admin + CSRF

Run vigembus_installer.exe /quiet. Non-Windows returns {status: false, error}.

Webhook payloads#

On stream start/stop SolarFlare POSTs application/json to every webhook_url_N (http(s) only, 5 s connect/timeout, 2 retries with backoff, no retry on 4xx). With webhook_secret set, each request carries X-Solarflare-Signature: sha256=HMAC(secret, body). Body: {event: stream.start|stream.end, t_start, t_end, app_name, client_name, client_address, codec, width, height, fps, avg_bitrate_kbps, avg_rtt_ms, avg_encode_ms, dropped_frames, error}. Identical to GET /api/sessions items except for the event wrapper — verify the HMAC before acting on callbacks.