SealSkin

HTTP API

How requests are wrapped and authenticated, and every endpoint the server exposes.

The API is what the served UI and the shells speak; it is documented here for people writing their own client or automation. FastAPI also publishes an OpenAPI description at /openapi.json and an interactive explorer at /docs on the API port, but neither can be used directly because of the encryption envelope described first.

Conventions

Base URL. https://<server>:8443 (through Caddy) or http://<server>:8000 (the API server directly). Both serve the same routes.

Handshake. Before anything else, establish a crypto session:

POST /api/handshake/initiate         -> {"nonce": b64, "signature": b64}
   verify: RSA-PSS(SHA-256, salt 32) over the nonce with the server public key
POST /api/handshake/exchange         {"encrypted_session_key": b64}
   RSA-OAEP(SHA-256) of a 32-byte AES key under the server public key
                                     -> {"session_id": hex}

Envelope. Every endpoint marked encrypted below expects the header X-Session-ID: <session_id> and, for requests with a body, {"iv": b64, "ciphertext": b64} where the ciphertext is AES-256-GCM of the JSON body with a 12-byte IV and no additional data. JSON responses come back in the same envelope. Optionally add X-Idempotency-Key: <random> to a non-GET request to make a retry safe.

Authentication. Endpoints marked user or admin require Authorization: Bearer <jwt>, an RS256 token signed with the user's private key whose claims include sub (the username), exp, and sid, the X-Session-ID it is sent with (a token without sid is refused on the API port itself and taken only through the node's TLS proxy), or, on the plain lane, Authorization: Bearer ssk_<id>_<secret>, an application key with the client's name in X-SealSkin-App (chrome, firefox, android, ios, else other). A refused key answers 401 with the code in detail: unknown, expired, disabled, client, or dormant. The companion apps call every endpoint under /companion/, as /companion/api/..., where a key is the one credential taken. admin endpoints additionally require an administrator account.

The plain lane. A request through Caddy that names no crypto session is taken and answered as plain JSON, with no envelope: it is how the web app, the companion apps with an application key, and the nodes of a cluster call every endpoint below. It authenticates with the cookie of a web sign-in or the header of a trusted proxy, both of which are honored only on a request the browser marks as coming from the web app's own pages (Sec-Fetch-Site: same-origin, else a matching Origin), with an application key, or with another node's signature. 409 answers a write another node got in ahead of, and 503 one the shared store could not take.

Errors. Standard HTTP status codes with a JSON {"detail": ...} body: 400 for a bad envelope or crypto session, 401 for a bad token, 403 for a permission the account lacks, 404, 422 for a body that fails validation, 500 for provider errors.

Handshake and status

Method and pathAuthPurpose
POST /api/handshake/initiatenoneSigned nonce for server verification.
POST /api/handshake/exchangenoneRegister the wrapped AES key; returns the crypto session id.
POST /api/admin/statusencrypted, userThe caller's role, effective settings, detected GPUs, CPU model, disk usage, and proxy certificate expiry.
GET /api/ui/versionnone{"version", "bridge"}: server version and bridge protocol version.
GET /api/ui/template_schemanoneThe template editor's variable definitions.

Web sign-in

Method and pathPurpose
GET /api/auth/configWhich sign-ins the server offers: oidc, saml, proxy, root (the root key), key (key files); auto names the provider a signed-out browser is sent to at once, or is empty.
GET /api/auth/oidc/login, GET /api/auth/saml/loginStart a provider flow; it ends at /#sso=<grant> or /#sso-error=<code>.
POST /api/auth/registerBody {grant}: start the sign-in and set its cookie.
POST /api/auth/keyBody {key}: sign the built-in administrator admin in with one of their application keys, the root key; 403 disabled where the node has SEALSKIN_ROOT_SIGN_IN=false, 403 notRoot for another user's key, 403 refused, 429 locked after five wrong keys from one address within a minute.
POST /api/auth/signoutEnd the sign-in the caller's cookie names and clear the cookie.
GET /api/auth/proxyWhat the request's sign-in headers held when it arrived and whether a trusted proxy passed it on: the path the server asks through its own public address to check the proxy. It signs nobody in.
GET /api/auth/saml/metadataThe service provider metadata to register with a SAML provider.

The provider's callbacks and logout notices (/api/auth/oidc/callback, /api/auth/oidc/backchannel-logout, /api/auth/oidc/frontchannel-logout, /api/auth/saml/acs, /api/auth/saml/slo) are for the provider.

Application keys

The user's own routes answer a web sign-in alone, the cookie's or the proxy's: a key, or a key-file client, cannot manage keys.

Method and pathAuthPurpose
GET /api/keyswebThe caller's keys (id, name, created, expires, last_used, client, state of active, dormant, or expired) and the policy over them: allowed, clients, max_days, dormant_hours, follow_sign_in, web_sign_in.
POST /api/keyswebBody {name, days?}: make a key, returned once as key, ssk_<id>_<secret>@<host>:<port> with the node's public address for the app to read, with its record; 403 disabled, 400 days over the longest lifetime.
DELETE /api/keys/{id}webRevoke one of the caller's keys.
POST /api/keys/wakewebCount the caller's sign-in as happening now, which wakes dormant keys; returns the listing.
GET /api/admin/users/{username}/keysadminA user's keys.
DELETE /api/admin/users/{username}/keys/{id}, DELETE /api/admin/users/{username}/keysadminRevoke one, or every, key of a user.

Applications and launching

Method and pathAuthPurpose
POST /api/applicationsencrypted, userApplications the caller may launch, with their capabilities and extensions.
GET /api/app_icon/{app_id}encrypted, userA custom (meta-app) icon as base64 JSON.
GET /api/logos/{name}noneA picture a store, a PRoot Apps remote, or a home link named, from this node's copy; the pages load every such picture here and never from its origin. 404 for a name the server never handed out.
POST /api/launch/simpleencrypted, userLaunch with no context. Body: application_id, home_name?, language?, timezone?, selected_gpu?, launch_in_room_mode?, wayland_mode?.
POST /api/launch/urlencrypted, userAs above plus url; the container receives SEALSKIN_URL.
POST /api/launch/fileencrypted, userAs above plus filename, upload_id, total_chunks, open_file_on_launch?; the uploaded file is placed in Desktop/files and, if requested, SEALSKIN_FILE points at it.
POST /api/launch/file_pathencrypted, user, persistent storageLaunch with a file already in the caller's shared files (filename).
GET /api/launch/progress/{launch_id}encrypted, userWhere a launch is. Any launch request may carry launch_id, a UUID the client makes up; this answers stage (placing, forwarding, storage, files, image, starting, waiting, ready, or failed), detail (node, image), elapsed, and, at the end, session_url and session_id or error.

A launch returns {"session_url", "session_id"}; session_url is relative to the session port and contains the one-time access token.

Method and pathAuthPurpose
GET /app/{app_id}/?home=&gpu=&room=&lang=&wayland=&where=webThe pinned address of an application: attaches to the oldest running session of it with the same home and room-or-not, else launches with the start-only options; /app/{app_id} without the slash redirects here.
GET /app/{app_id}/manifest.jsonwebThe web app manifest that installs the address as its own app, with the same query.
GET /app/{app_id}/icon.pngnoneThe application's icon, which an installing browser asks for without the sign-in.
GET /{session_id}/manifest.json, icon.png, icon-512.png, favicon.icosessionA running session's own manifest and icons, built from its record.

Sessions

Method and pathAuthPurpose
GET /api/sessionsencrypted, userThe caller's sessions, newest first.
DELETE /api/sessions/{session_id}encrypted, userStop one of the caller's sessions.
GET /api/sessions/{session_id}/screenshotencrypted, userA thumbnail of the session's desktop, {"image", "width", "height", "taken_at"}, with image a JPEG data URI, a recent capture unless ?fresh=1 asks for a new one; 404 while the container has none to give.
POST /api/sessions/{session_id}/send_fileencrypted, userMove a finished upload (filename, upload_id, total_chunks) into the session's files directory.
GET /{session_id}/access tokenExchange ?access_token= for the session cookie and redirect. Add &embedded=true for a SameSite=None cookie.
GET /room/{session_id}access or collaboration tokenThe collaboration room page.
WS /ws/room/{session_id}collaboration tokenThe room's WebSocket.

Everything under /{session_id}/ other than the initial exchange is proxied by Caddy to the container after forward_auth.

Home directories

Method and pathAuthPurpose
GET /api/homedirsencrypted, user, persistent storageThe caller's home directories.
POST /api/homedirsencrypted, user, persistent storageCreate one (home_name, letters, digits, _, -).
DELETE /api/homedirs/{home_name}encrypted, user, persistent storageDelete one and its contents.

Uploads

Uploads are chunked so that large files fit within the encrypted envelope. Upload ids are lowercase UUIDs scoped to the uploading user.

Method and pathAuthPurpose
POST /api/upload/initiateencrypted, user{"filename", "total_size"} → {"upload_id"}.
POST /api/upload/chunkencrypted, user{"upload_id", "chunk_index", "chunk_data_b64"}.
POST /api/upload/to_storageencrypted, user, persistent storageFinalise into the shared files of home_name (filename, upload_id, total_chunks).

Files

All paths are relative to the named home directory (or _sealskin_shared_files).

Method and pathAuthPurpose
GET /api/files/list/{home_dir}?path=&page=&per_page=&search=encrypted, user, persistent storageA page of directory entries.
POST /api/files/create_folder/{home_dir}encrypted, user, persistent storage{"path", "folder_name"}.
POST /api/files/delete/{home_dir}encrypted, user, persistent storage{"paths": [...]} → a task id; deletion runs in the background.
GET /api/files/delete_status/{task_id}encrypted, userProgress of a deletion.
GET /api/files/download/chunk/{home_dir}?path=&chunk_index=encrypted, user, persistent storageOne chunk of a file, base64, with is_last_chunk.
POST /api/files/upload_to_dir/{home_dir}encrypted, user, persistent storageFinalise an upload into a directory (path, filename, upload_id, total_chunks).
POST /api/files/launch_from_storageencrypted, user, persistent storageReturns a launch context for a server-side file; the client opens the launcher with it.

Public shares

Method and pathAuthPurpose
POST /api/files/shareencrypted, user, public sharing{"home_dir", "path", "password"?, "expiry_hours"?} → share info including its URL.
GET /api/files/sharesencrypted, user, public sharingThe caller's shares.
DELETE /api/files/share/{share_id}encrypted, user, public sharingRevoke a share.
GET /public/{share_id}noneThe file, or a password form.
POST /public/{share_id}noneForm field password; redirects to a one-time download URL. Five wrong passwords within a minute answer 429 until the oldest ages out.
GET /public/download/{token}noneDownload with a one-time token while the share has not expired.

Administration

All encrypted, admin.

Method and pathPurpose
POST /api/admin/dataAdmins, users, groups, server public key, ports, and GPUs in one call.
POST /api/admin/admins, DELETE /api/admin/admins/{username}Create (username, public_key?) or delete an administrator.
POST /api/admin/usersCreate a user (username, public_key?, settings); returns the generated private key when no key was supplied.
PUT /api/admin/users/{username}Replace a user's settings.
DELETE /api/admin/users/{username}Delete a user and their storage.
POST /api/admin/users/{username}/approveLet a held user in, placed in the groups the body names; none leaves them in default.
GET/POST/DELETE /api/admin/users/{username}/homedirs[/{home}]A user's home directories.
GET/POST/DELETE /api/admin/admins/{username}/homedirs[/{home}]An administrator's home directories.
POST /api/admin/groups, PUT /api/admin/groups/{name}, DELETE /api/admin/groups/{name}Groups. default, the built-in group of users in no other, is listed with the rest but takes none of these (409), nor does the name all or none.
GET/POST /api/admin/apps/stores, DELETE /api/admin/apps/stores/{name}App stores.
GET /api/admin/apps/available?url=&store_name=&refresh=A store's catalogue, cached unless refresh.
GET /api/admin/apps/installedInstalled apps with image status.
POST /api/admin/apps/installedInstall from a store entry.
PUT /api/admin/apps/installed/{app_id}Replace with a full definition.
PATCH /api/admin/apps/installed/{app_id}Partial update: record fields (users, groups, app_template, auto_update) apply directly, anything else becomes an override.
DELETE /api/admin/apps/installed/{app_id}Uninstall (purges a meta-app's icon and template).
POST /api/admin/apps/installed/{app_id}/check_updateCompare the local image digest with the registry.
POST /api/admin/apps/installed/{app_id}/pull_latestPull the newest image.
POST /api/admin/apps/metaCreate a meta-app (name, base_app_id, logo, autostart scripts, users, groups).
POST /api/admin/launch/meta_customizeLaunch a meta-app with its template mounted read-write; 409 while the caller has one open.
GET/POST /api/admin/apps/templates, DELETE /api/admin/apps/templates/{name}Templates; also open to a user allowed to edit app templates, short of their DOCKER_* and KUBERNETES_* settings.
GET /api/admin/sessionsEvery session grouped by user.
DELETE /api/admin/sessions/{session_id}Stop any session.
PUT /api/admin/brandingReplace the brand's record (name, logo_link, accent, apps with a client, url, and enabled per companion app, the wallpaper_* fields, links); the pictures stay.
DELETE /api/admin/brandingBack to the defaults: the record and the pictures are removed.
POST/DELETE /api/admin/branding/{logo|wallpaper}Body {data}, a base64 PNG, JPEG, or WebP (1 MB for the logo, 8 MB for the wallpaper): keep or remove a picture.
GET /api/admin/proot/catalogs, POST /api/admin/proot/catalogs, PUT/DELETE /api/admin/proot/catalogs/{id}, POST /api/admin/proot/catalogs/{id}/updatePRoot Apps catalogs: list them with the state of this node's copy, create, replace, delete, or fetch one again on every node.
GET /api/admin/proot/remote?repo=The apps a PRoot Apps remote (a GitHub owner/repo) publishes.
GET /api/admin/vpn, POST /api/admin/vpn/importVPN profiles with their gateways; make profiles of an uploaded WireGuard or OpenVPN file or archive (multipart file, kind, credentials).
PUT/DELETE /api/admin/vpn/{id}, POST /api/admin/vpn/{id}/endpointsChange a profile's name, published endpoints, cap, access, or credentials; delete it, its gateways staying up until their sessions end; add the endpoints of another file.

Branding

Method and pathAuthPurpose
GET /api/brandingencrypted, userThe brand with its pictures, as the home page reads it: the record, logo and wallpaper (url, width, height, type, or null), and is_default.
GET /api/branding/logo, GET /api/branding/wallpapernoneThe uploaded pictures, addressed with ?v=<hash> and cached for a year; 404 where none was uploaded.

Every page the server serves carries the brand in its head: an application-name meta, a sealskin-brand meta holding the name, logo address, logo link, accent, and store-links switch as JSON, and a style rule deriving the accent palette.

Cluster

Method and pathAuthPurpose
GET /api/clusteruserThe pools and nodes open to the caller, where their home directories are, and their limits.
POST /api/cluster/homedirs/{home}/moveuserBody {node}: move one of the caller's home directories, where their settings allow.
GET /api/admin/clusteradminNodes with their load, pools, the store, the cluster's sign-in settings, the state of the reverse proxy check, and how this request reached the node (signin.arrival).
POST /api/admin/cluster/join_codesadminBody {pool?}: a code one node joins with.
PUT/DELETE /api/admin/cluster/nodes/{id}adminApprove or suspend a node (approved), move it (pool), or remove its record.
PUT/DELETE /api/admin/cluster/pools/{name}adminCreate, replace, or delete a pool.
PUT /api/admin/cluster/settingsadminWrite sign-in settings for the whole cluster; an empty value hands one back to each node's environment.
GET /api/admin/cluster/usage?period=adminWeighted hours per user this day, week, or month.
GET /api/admin/cluster/audit?day=&since=&q=&offset=&limit=adminAudit events from every answering node, newest first: one day, or every day since one, containing every word of q; answers total, the page of events, and the days that have a log.
POST /api/admin/cluster/signin/testadminBody {kind}: for oidc or saml, fetch the configured provider's metadata and answer ok with what it published, or the error; for proxy, run the check of the reverse proxy again and answer its state and detail.
GET/DELETE /api/admin/labadminThe caller's open App Laboratory session, which no session list shows; deleting closes it and keeps its home directory as the template, answering its files and bytes, and with progress_id reports stopping and saving to the progress endpoint.
GET /api/admin/cluster/users/{username}/homesadminWhich node holds each of a user's home directories.
POST /api/admin/cluster/users/{username}/homedirs/{home}/moveadminBody {node}: move a user's home directory.

A launch request may name a pool or a node to start on. A request about a session or a home directory on another node is forwarded there and answered with that node's answer.

Internal

GET /internal/resolve_session/{session_id} and GET /internal/route/{session_id} exist only for Caddy's forward_auth; Caddy refuses them on its listeners and the API server only receives them over loopback. The first answers 200 with X-Upstream-Host and X-Upstream-Auth headers, or with X-Upstream-Peer for a session another node runs, or 403/404; the second answers only whether another node runs the session.

/peer/* is what nodes call on each other's peer listener, each request signed with the calling node's server key: status, change notices, joining, the shared records of a node that keeps them, uploads a frontend passes on, home directories being moved, and the storage of a user a frontend deleted.