Lab Kiosk OS & Edge SaaS

REST API Reference

The complete endpoint catalogue for the Lab Kiosk Cloudflare control plane, plus the client agent's loopback API.

Endpoints below are verified against cloudflare-control/src/index.ts. Where the in-repo docs/API.md and the implementation disagree, this page follows the implementation — see Known documentation drift.


Conventions

  • Protocol: HTTPS only in production, enforced by HSTS and edge redirection.
  • Content type: application/json; charset=utf-8, except for HTML pages and redirects.
  • Caching: API responses carry Cache-Control: no-store and X-Content-Type-Options: nosniff.
  • Errors: { "error": "<human-readable message>" } with the appropriate status.

Authentication schemes

SchemeHeaderUsed by
Session cookieCookie: labkiosk_session=<hex32> (HttpOnly; Secure; SameSite=Lax)Operator Lab Dashboard, Super Admin console
Device bearer tokenAuthorization: Bearer <hex32>agent.py on each workstation, for /api/devices/ws, /api/telemetry and /api/devices/boot-report
Public / key-exchangednone, or a one-time enrollmentKey in the bodyLanding page, legal pages, sign-in, registration, user portal, enrolment, health probe

Session tokens are random 32-byte hex strings; only their SHA-256 hash is stored in D1. Device tokens are handled the same way — the plaintext token exists only on the workstation that was issued it.

Guards

Every route passes through src/guard.ts before its handler runs:

GuardRejects withApplies to
resolveTenant()404every tenant-scoped route
requireTenantAdmin()401 anonymous, 400 no organization, 403 wrong tenant (and a super admin outside its own demos)GET /api/broadcast-presets, GET /api/whitelist, the /admin pages
requireTenantPermission(…, permission)as above, plus 403 without the permissionsettings: /api/settings/*, /api/tenant/subdomain, /api/tenant/homepage, /api/tenant/settings, /api/audit-logs, /api/workstation-issues; workstations: /api/clients*, /api/console/ws, /api/console/remote, /api/groups*; staff: /api/tenant/staff*; portal: mutating /api/portal-sites*; broadcast: mutating /api/broadcast-presets*; whitelist: POST /api/whitelist; /api/command: broadcast for navigate, workstations otherwise
requireSuperAdmin()401 / 403all /api/super/*
requireDevice()401/api/devices/ws, /api/telemetry, /api/devices/boot-report, /api/devices/remote
rejectCrossSiteMutation()403every cookie-authenticated POST/DELETE under /api/

Rate limiting

  • Sign-in: repeated failures trigger exponential back-off per identifier, answered with 429 Too Many Requests.
  • Registration and failed enrolment: throttled per source address via rateLimitWait() / recordRateLimitHit() in db.ts.

Payload limits

  • Thumbnails: capped at 256 KB. Anything larger, or not prefixed data:image/jpeg;base64, or data:image/png;base64,, is dropped server-side. The agent drops oversized frames before sending, so the heartbeat still lands.
  • Lock messages: truncated to 280 characters.
  • VNC passwords: truncated to 64 characters (x11vnc itself uses only the first 8 — see Remote Control).
  • Client identifiers: must match ^[A-Z0-9][A-Z0-9_-]{0,63}$ (enrolment in index.ts, the hub in org_hub.ts). The agent is stricter, {0,62}, so a wizard-chosen id always passes.
  • URLs: every navigable URL passes safeHttpUrl(), which accepts only http:/https: and prepends https:// to scheme-less domains.

Endpoint summary

Public

EndpointMethodDescription
/api/statusGETPlatform health and the tenant's current kiosk target
/api/auth/register/email-codePOSTEmail a six-digit registration code
/api/auth/registerPOSTRegister an organization for review
/api/contact/email-codePOSTEmails the code that proves the sender's address ({ email, turnstileToken? })
/api/contactPOSTThe contact page's message ({ name, organization?, email, emailCode, reason, message }); filed in Mail under its reason
/api/auth/loginPOSTSign in as operator or super admin
/api/portal-sitesGETList the host tenant's user portal cards
/api/devices/enrollPOSTExchange an enrollment key for a device token

Session-authenticated

EndpointMethodDescription
/api/auth/meGETCurrent user profile and tenant
/api/auth/logoutPOSTInvalidate this session and clear the cookie
/api/auth/change-passwordPOSTRotate password, revoking the account's other sessions and trusted browsers
/api/auth/login/verifyPOSTSecond step of a two-factor sign-in (app code, emailed code or recovery code)
/api/auth/login/email-codePOSTEmail a sign-in code for a pending two-factor sign-in
/api/auth/two-factor[/setup,/enable,/recovery-codes,/disable]GET/POSTThe signed-in account's own two-factor sign-in

Operator admin

EndpointMethodDescription
/api/clientsGETFleet state with live thumbnails and remote-control details
/api/clients/remote-sessionPOSTOpen a Remote Control session through the console's relay; asks the workstation to join (409 when it is not connected)
/api/console/remoteGET (WebSocket)The viewer's side of a Remote Control session (VNC bytes)
/api/clients/removePOSTDecommission a workstation and revoke its token
/api/clients/groupPOSTAssign workstations to a group
/api/groupsGET POSTList and create workstation groups
/api/groups/:idDELETEDelete a workstation group
/api/tenant/staffGET POSTList and create delegated staff (requires staff; see the delegation limits in docs/API.md)
/api/tenant/staff/updatePOSTChange a staff account's role or permissions
/api/tenant/staff/:idDELETERemove a staff account and end its sessions
/api/commandPOSTDispatch a command to one, selected, or all workstations
/api/whitelistGET POSTRead and modify the permanent domain allowlist
/api/portal-sitesPOSTAdd a user portal card
/api/portal-sites/:idDELETERemove a user portal card
/api/broadcast-presetsGET POSTList and add quick-launch broadcast shortcuts
/api/broadcast-presets/:idDELETERemove a broadcast shortcut
/api/settings/modePOSTSwitch between portal and single_url
/api/settings/customizationGET POSTOrganization branding, portal copy, default lock message
/api/settings/subdomainPOSTRequest a subdomain change
/api/settings/enrollment-keyGET POSTView or rotate the enrollment key
/api/settings/custom-domainPOST DELETERequest or disconnect a custom domain
/api/audit-logsGETPaginated organization audit log
/api/workstation-issuesGETErrors and warnings workstations reported (Settings → Errors & Warnings), newest first, with each one's bug report state; plus the organization's automatic bug report settings (requires settings)
/api/settings/bug-reportsPOSTOpt in to or out of automatic, redacted GitHub bug reports; turning them on accepts the current terms version (requires settings)

Device

EndpointMethodDescription
/api/devices/wsGET (WebSocket)The control channel: status and frames up; configuration, commands and frame requests down
/api/telemetryPOSTThe HTTP fallback: a three-second heartbeat carrying the same
/api/devices/boot-reportPOSTAn installed workstation's boot outcome (update installed, failed, rolled back, fallback, error)
/api/devices/remoteGET (WebSocket)The workstation's side of a Remote Control session, with X-Labkiosk-Session; piped to its loopback x11vnc

Super admin

EndpointMethodDescription
/api/super/inboxGETTasks (registrations, Remote Control requests) or Mail, ?box=tasks|support&mailbox=<address>
/api/super/inbox/composePOSTWrite a new email as any address on the mail domain
/api/super/inbox/:idGETOne conversation, its messages and registration details
/api/super/inbox/:id/{reply,note,status,delete,verify-phone,approve,reject}POSTAnswer by email, note, close, delete mail, confirm the phone, decide
/api/super/inbox/:id/attachment/:messageId/:indexGETDownload an attachment, or original for the whole message
/api/super/tenants/approvePOSTApprove a requested subdomain change
/api/super/tenants/rejectPOSTDecline a requested subdomain change
/api/super/tenants/suspendPOSTSuspend an active organization
/api/super/tenants/reactivatePOSTReactivate a suspended organization
/api/super/tenants/custom-domain/approvePOSTApprove and bind a custom domain
/api/super/tenants/custom-domain/rejectPOSTReject a requested custom domain
/api/super/tenants/custom-domain/removePOSTUnbind an assigned custom domain

HTML routes

PathServes
/Landing page on the platform apex; User Portal on a tenant host
/Organization homepage (organization-authored headline, intro and content blocks)
/homeUser Portal (approved app grid)
/adminOperator Lab Dashboard
/superSuper Admin Master Console
/login, /register, /contactLanding-page sections
/privacy, /termsPrivacy Policy and Terms of Service
/terms/bug-reportsThe Automatic Bug Report Terms (public)
/download, /isoRedirect to ISO_DOWNLOAD_URL

Detailed reference

POST /api/devices/enroll

Exchanges an organization's enrollment key for a persistent device bearer token. This is the only moment a workstation proves who it is with a shared secret; afterwards it holds its own token.

Access: public, throttled per source address on failure.

{
  "subdomain": "oakridge",
  "clientId": "PC-01",
  "enrollmentKey": "KEY-ABCD-1234-EFGH",
  "customDomain": "kiosk.oakridge.edu"
}

customDomain is optional; the agent sends it when the workstation was pointed at a custom domain rather than a platform subdomain.

**200 OK**

{
  "status": "ok",
  "deviceToken": "32_byte_hex_bearer_token",
  "clientId": "PC-01",
  "subdomain": "oakridge",
  "organizationName": "Oakridge Holdings",
  "schoolName": "Oakridge Holdings",
  "mode": "portal",
  "targetUrl": "https://oakridge.labkiosk.example.com"
}

schoolName is also sent, with the same value, for agents installed from an ISO older than the organization vocabulary. It is deprecated: new code reads organizationName, and the field will be removed once no workstation in the field depends on it.

The agent writes the token to /etc/labkiosk/config.json with mode 0600. On live media that file lives in the RAM overlay and is lost at power-off, which is intended — the workstation is meant to be installed. On an installed disk, /etc/labkiosk is a mount point for the LABKIOSK_DATA partition, which is what makes a post-install enrolment persist.

Failure modes: an empty or wrong key, an unapproved or suspended organization, or a clientId failing CLIENT_ID_PATTERN are all rejected. Organizations begin with an empty enrollment key, which authenticates nothing until an operator generates one.


GET /api/devices/ws

The workstation's control channel: a WebSocket to its organization's OrgHub. Current agents use it whenever the image has python3-websocket.

Access: Authorization: Bearer <deviceToken> on the upgrade. 401/403 when the token or the organization is refused, 426 without an upgrade.

DirectionMessage
hub → workstation{"type":"config", whitelist, mode, targetUrl, broadcastUrl, broadcastEpoch, commands?} on connect and after every admin change
hub → workstation{"type":"commands", commands} the moment a command is dispatched
hub → workstation{"type":"frames", on, intervalSeconds} when a console starts or stops showing this screen
workstation → hub{"type":"status", clientNum, activeUrl, isLocked, vncPassword?} on connect and on change
workstation → hub{"type":"frame", thumbnail} every intervalSeconds while asked
workstation → hub{"type":"ping"} every 15 s, byte for byte; answered {"type":"pong"} at the edge

Close codes: 4001 the workstation was removed, 4003 the organization is not active (both send the screen to re-enrolment), 4000 replaced by a newer connection, 4008 silent for 75 s.

GET /api/console/ws

The Workstations page's live channel. Access: a session with the workstations permission and an Origin of this site. The console sends {"type":"watch", clientIds} for the screens it is showing and receives snapshot, status, frame and removed messages.

POST /api/telemetry

The HTTP heartbeat, called every three seconds by agents without the WebSocket client (and by any agent whose server has no WebSocket route). It carries the same state as the control channel in one request and reply.

Access: Authorization: Bearer <deviceToken>.

Request — exactly these keys; post_telemetry() in agent.py is the reference implementation:

{
  "clientNum": 1,
  "activeUrl": "https://scratch.mit.edu",
  "isLocked": false,
  "thumbnail": "data:image/jpeg;base64,...",
  "vncPassword": "a1b2c3d4"
}
FieldNotes
clientNumInteger workstation number; non-numeric values fall back to 1.
activeUrlValidated by safeHttpUrl(); falls back to the tenant default.
isLockedWhether the lock curtain is currently up.
thumbnailBase64 JPEG from scrot -t 20 -q 35. Omitted when the encoded payload would exceed MAX_THUMBNAIL_BYTES (256 KB), so an oversized frame is dropped rather than allowed to bloat a three-second loop. No PIL/Pillow is involved — the agent is standard library only.
vncPasswordPer-boot ephemeral secret from /tmp/labkiosk/vnc.secret. Sent only when present, so the control plane keeps what it already knows otherwise.

There is **no currentUrl key and no metrics object.** The agent collects no CPU, RAM, or storage statistics; do not build a dashboard against fields that do not exist.

Identity is taken from the token, never the body. Any clientId or tenant the payload claims is discarded.

**200 OK**

{
  "status": "ok",
  "commands": [
    { "id": "cmd-123", "action": "lock", "message": "Eyes to the board please." }
  ],
  "whitelist": ["scratch.mit.edu", "khanacademy.org", "oakridge.labkiosk.example.com"],
  "mode": "portal",
  "targetUrl": "https://oakridge.labkiosk.example.com",
  "broadcastUrl": "",
  "broadcastEpoch": 0
}
FieldNotes
commandsPending commands for this workstation. The hub records each delivery, so each command executes exactly once rather than on every heartbeat.
whitelistEffective allowlist: the organization's own domains plus every portal app host, plus the active broadcast host if it is not already present. Merged into the Chromium managed policy.
targetUrlWhere the kiosk should point. Validated as http(s) by safe_navigable_url() before the agent stores it, because it ends up in window.location.
broadcastUrl / broadcastEpochThe authoritative synchronised page. The epoch is a monotonic marker letting a workstation distinguish a new broadcast from a replayed one.

**403** if the organization is not active — a suspended organization's workstations stop receiving commands and policy.


POST /api/devices/boot-report

What an installed workstation's last boot did with its system image, as labkiosk-boot-slots check recorded it in /run/labkiosk-update/status.json. The agent sends only the outcomes worth an administrator's attention; the latest is kept on the workstation's row (client_devices.image_version, update_state, update_error, update_state_at). A failure, rollback, fallback or error is also listed in workstation_issues (Settings → Errors & Warnings) as update_failed, update_rolled_back, boot_error (severity error) or boot_fallback (warning). None of it goes to the audit log, which records what people did.

Access: Authorization: Bearer <deviceToken>; the token decides the organization and the workstation.

Request: state (installed | failed | rolled-back | fallback | error), version (the image running), at (when the workstation recorded it, Unix seconds, within the last 7 days); optional previous, failed (release versions) and error (text, cut to 300 characters).

{ "state": "rolled-back", "version": "2.5.1", "failed": "2.6.0", "at": 1791100000 }

**200 OK** → { "status": "ok", "recorded": true }; recorded: false for a report the workstation already sent (or one less than 60 s after the last). **400 malformed, not sent again. 409** the workstation has not checked in yet, sent again later.


GET /api/workstation-issues

Errors and warnings the organization's workstations reported, newest first (?limit=, default 100, at most 200; rows older than 90 days are deleted by the hourly cron).

Access: organization admin or staff with the settings permission.

Each issue carries id, client_id, severity, kind, image_version, details, occurred_at, created_at, report_state (none | pending | sent), report_match (new | existing), and, once sent, issue_url, issue_number, report_status (open | in_progress | pr_open | resolved | closed) and pr_url. The response also carries the organization's opt-in state:

{
  "issues": [ … ],
  "bugReports": {
    "enabled": false,
    "available": true,
    "repository": "owner/repo",
    "termsVersion": "2026-10-04",
    "acceptedTermsVersion": null,
    "termsAcceptedAt": null
  }
}

available is false, and repository null, unless the platform has set up the AI binding, GITHUB_ISSUES_TOKEN and a valid GITHUB_ISSUES_REPO.

POST /api/settings/bug-reports

Opts the organization in to or out of automatic, redacted GitHub bug reports. Turning them on accepts the current Automatic Bug Report Terms (/terms/bug-reports), by version.

Access: organization admin or staff with the settings permission.

{ "enabled": true, "acceptTerms": "2026-10-04" }
{ "enabled": false }

**200 OK** → { "status": "ok", "enabled": true }, with an audit-log row (settings.bug_reports). **400** when enabled is not a boolean, or when turning on without the current terms version. **409** when the platform has not set automatic bug reports up.


POST /api/command

Dispatches a remote action to one workstation or the whole lab.

Access: organization admin.

Supported actions (ALLOWED_COMMANDS in index.ts, mirrored by execute_command() in agent.py):

ActionEffect on the workstation
lockRaises the full-screen lock curtain across every tab, with the message.
unlockDrops the curtain.
navigateNavigates top-level to url. With resetPortal: true, returns to the organization portal and clears the broadcast.
reloadReloads the current page.
rebootReboots the workstation through logind.
shutdownPowers the workstation off.
muteMutes audio output via alsa-utils.

Anything else is rejected with 400 Unsupported action. There is no broadcast action — a broadcast is navigate to target: "all", which additionally writes broadcast_url and broadcast_epoch onto the tenant row.

{ "targets": ["PC-01", "PC-02"], "action": "lock", "message": "Midterm examination is beginning." }
{ "target": "all", "action": "navigate", "url": "https://scratch.mit.edu" }
{ "target": "all", "action": "navigate", "resetPortal": true }

Targeting supports either targets: string[] (array of clientId strings) or target: string ("all" or a single clientId). Duplicates are dropped, "all" replaces named targets, and at most 500 targets are accepted. When action is lock and no message is supplied, the tenant's default_lock_message is used.

**200 OK** → { "status": "ok", "commandId": "cmd-uuid-99" }

The hub pushes the command to a connected workstation at once; the console sees the new state as soon as the workstation reports it. Every dispatch writes an audit-log row (command.<action>).


GET /api/clients

Returns the organization's fleet: the D1 registry merged with the hub's live status. POST with {"watch": ["PC-01", …]} also asks the hub for those screens' frames for the next 10 seconds -- the fallback for a console without its live channel; thumbnail is present only for watched, online workstations.

Access: organization admin.

{
  "clients": {
    "PC-01": {
      "clientId": "PC-01",
      "clientNum": 1,
      "activeUrl": "https://scratch.mit.edu",
      "isLocked": false,
      "thumbnail": "data:image/jpeg;base64,...",
      "timestamp": 1726300000,
      "lastSeen": "2026-09-14T09:26:40.000Z",
      "online": true,
      "vncPassword": "a1b2c3d4"
    }
  }
}

vncPassword is what makes one-click remote control work without an operator typing anything. It is readable only by an authenticated admin of that specific organization. A remoteHost an older agent still sends is ignored.


POST /api/clients/remove

Decommissions a workstation and revokes its device token. The machine's next heartbeat fails with 401 and it stops appearing on the dashboard.

{ "clientId": "PC-01" }

→ { "status": "ok", "remaining": 14 }


POST /api/auth/register

Creates an organization and its first administrator for review. Ask for emailCode first with POST /api/auth/register/email-code { "email" }. The organization starts pending: nobody is signed in, it cannot sign in or enrol workstations, and the contact is emailed. A super admin confirms the phone number and approves it under Super Admin → Tasks, which emails the console address.

{
  "name": "Oakridge Holdings",
  "legalName": "Oakridge Holdings Pvt Ltd",
  "organizationType": "business",
  "contactName": "Jane Smith",
  "email": "it@oakridge.example",
  "emailCode": "482913",
  "phone": "+91 98765 43210",
  "password": "StrongPassword123!",
  "subdomain": "oakridge",
  "addressLine1": "12 Market Road",
  "city": "Bhubaneswar",
  "region": "Odisha",
  "postalCode": "751001",
  "country": "India",
  "taxId": "21ABCDE1234F1Z5",
  "billingEmail": "accounts@oakridge.example",
  "workstationEstimate": 40,
  "acceptTerms": true
}

→ { "status": "ok", "pending": true, "reference": "LK-7Q2M4K" }

Reserved slugs are refused. Passwords are checked by validatePasswordStrength() and stored as PBKDF2-HMAC-SHA256, 100 000 iterations, 32-byte random salt, 256 derived bits.


POST /api/auth/login

{ "email": "operator@oakridge.edu", "password": "StrongPassword123!" }

→ { "status": "ok", "role": "org_admin", "subdomain": "oakridge", "redirect": "…" }, plus the labkiosk_session and labkiosk_device cookies. An account with two-factor sign-in, on a browser it has not trusted, gets { "status": "two_factor", "challenge": "…" } instead, completed with POST /api/auth/login/verify.

Repeated failures back off exponentially per identifier, tracked in login_attempts.


POST /api/auth/change-password

Rotates the password and revokes the account's other sessions, so a stolen cookie does not survive a password change. This is the only path by which a password changes.

{ "currentPassword": "OldPassword123!", "newPassword": "NewStrongPassword456!" }

GET / POST /api/portal-sites

GET is public and scoped to the host tenant; it is what the User Portal renders from. POST requires organization admin.

{
  "title": "Scratch Programming",
  "url": "https://scratch.mit.edu",
  "category": "Computer Science",
  "icon": "🐱",
  "thumbnailUrl": "https://example.com/scratch.jpg"
}

Adding a card implicitly authorises its host: buildEffectiveWhitelist() unions the permanent allowlist with every portal app domain, so an operator never has to add a site in two places.


POST /api/settings/mode

{ "mode": "single_url", "defaultUrl": "https://canvas.example.edu" }

safeHttpUrl() prepends https:// to a scheme-less domain, so canvas.example.edu is accepted.


POST /api/settings/customization

{
  "name": "Oakridge STEM Academy",
  "defaultLockMessage": "Examination active. No talking.",
  "portalTitle": "Digital Learning Lab",
  "portalSubtitle": "Select an approved page to begin",
  "portalDescription": "Computer Science Lab 304",
  "portalFooter": "For technical assistance, raise your hand."
}

Every one of these strings is attacker-controlled from the platform's perspective and is escaped on render. The test suite asserts that hostile input renders inert.


POST /api/settings/enrollment-key

Rotates the key. Already-enrolled workstations keep their bearer tokens and are unaffected — rotation only prevents new enrolments with the old key.

→ { "status": "ok", "enrollmentKey": "KEY-WXYZ-7890-HIJK" }


Super admin endpoints

All take a tenantId and require a super_admin session.

{ "tenantId": "tenant-uuid-1", "subdomain": "oakridge" }

Suspension is the platform's kill switch: a suspended organization's workstations receive 403 on telemetry, stop getting commands and policy, and its portal stops serving.

Custom domain approval binds an FQDN to a tenant. Once bound, Cloudflare routes it to the worker and resolveTenant() recognises it from the Host header. → Super Admin Guide


The client agent's loopback API

agent.py also serves a small HTTP API on **127.0.0.1:8888**, used only by the local setup wizard. It is not reachable from the network, from the organization LAN, or from a visited web page.

Every request must satisfy both _is_expected_host() (the Host header is loopback) and _is_local_caller() (the Origin, when present, is 127.0.0.1 or localhost). Either check failing returns 403.

One further origin is accepted: the kiosk extension's own origin (chrome-extension://hfjmbeplebjipenkfabncgkpadnjmmoe, pinned by the key in manifest.json). Chromium stamps every non-GET fetch from the extension's service worker with it, and that worker is what the top bar's administrator modal uses to reach POST /api/admin/verify.

EndpointMethodDescription
/setupGETServes wizard.html. Returns 403 once the workstation is enrolled unless accessed via #network with admin authentication.
/api/statusGETLocal state: clientId, clientNum, isLocked, lockMessage, targetUrl, broadcastUrl, broadcastEpoch, isConfigured, baseDomain, isLive, isInstalled, isOnline, persistentStorage, installRequested. persistentStorage is false when /etc/labkiosk is not the LABKIOSK_DATA partition, i.e. an enrolment made now would not survive a reboot.
/api/localization/optionsGETContinents, countries, timezones, locales, keyboard layouts and interface catalogs, all read from the workstation's own tzdata, locale and X11 tables.
/api/localization/languagesGETInterface languages the organization's control plane offers, with the installed ones marked.
/api/localization/language/downloadPOSTDownloads one catalog from the control plane into /etc/labkiosk/i18n. Administrator token required once installed.
/api/localization/configurePOSTApplies language, region, timezone, keyboard and (when syncTime is false) the clock by hand. Administrator token required once installed.
/i18n/<tag>.jsonGETAn interface catalog. en-US is bundled; others come from /etc/labkiosk/i18n.
/api/network/statusGETComprehensive network status: active device, IPv4/IPv6 addresses, gateway, DNS, proxy, interfaces, and connectivity check.
/api/network/interfacesGETList of hardware interfaces with device name, type (ethernet / wifi), state, and physical carrier link status.
/api/network/wifi/scanGETLive Wi-Fi scan results: SSID, BSSID, signal strength (0-100), channel, security mode, and encrypted flag.
/api/network/configurePOSTConfigures and connects interface (Ethernet/Wi-Fi) with IPv4/IPv6 mode (auto, custom_dns, manual), DNS, and optional HTTP proxy.
/api/network/testPOSTProbes DNS resolution and internet route reachability (1.1.1.1:53 / 8.8.8.8:53).
/api/logGETTail of /tmp/lab-agent.log (max 64 KB, text/plain). Needs the X-LabKiosk-Admin token on an installed workstation. Shown by the wizard's Agent Log & Diagnostics panel.
/api/admin/verifyPOSTVerifies administrator password against the GRUB PBKDF2 hash (/run/live/medium/boot/grub/labkiosk-password.cfg: boot/grub on the installed disk's LABKIOSK_ROOT, outside every system image) and returns a 10-minute token for /api/network/configure (header X-LabKiosk-Admin, required on installed systems). Throttled: 5 failures lock it for 60 s.
/api/install/disksGETCandidate target disks. Returns [] when not a live session.
/api/install/statusGETInstallation state and progress percentage.
/api/installPOSTStarts the disk install. 400 when the system is already installed.
/api/rebootPOSTReboots the workstation.
/api/setupPOSTPerforms enrolment against the control plane. 409 once already enrolled.

POST /api/install takes targetDisk (re-validated against TARGET_DISK_PATTERN) and an optional grubPasswordHash (re-validated against GRUB_PBKDF2_PATTERN). Only a digest is accepted: the wizard derives PBKDF2 in the browser with WebCrypto, so the plaintext boot-menu password never crosses the agent's API, never appears in a process argument, and is never written to disk.

POST /api/network/configure manages NetworkManager connections. On installed machines, connection keyfiles are persisted in LABKIOSK_DATA (/etc/labkiosk/system-connections/) and bind-mounted to /etc/NetworkManager/system-connections by a mount unit in the image (no /etc/fstab is written) so configurations persist across overlayroot="tmpfs" reboots.

→ Client Agent · Disk Installer


Known documentation drift

None currently tracked. docs/API.md was brought back in line with ALLOWED_COMMANDS in src/index.ts (lock, unlock, navigate, reload, reboot, shutdown, clear-session, mute); ALLOWED_COMMANDS and CommandAction in src/types.ts remain the authority.

This page is wiki/REST-API-Reference.md in the repository.