Skip to main content

Core HTTP Routes

This page is the working contract between playroll-ui and playroll-cpp for the localhost core API.

The renderer must not call these routes directly. playroll-ui calls core from the Electron main process through typed IPC handlers, and core requests must carry the UI/core session-auth headers unless an explicit local test bypass is enabled.

Route inventory status

The Paper UI handoff does not currently introduce a new localhost HTTP route. playroll-ui still talks to playroll-cpp through the same main-process IPC surface and core API paths.

Integration work is therefore contract work: payload shape, stable reason codes, lifecycle semantics, route auth, ordering guarantees, and UI read-model behavior.

Contract work that matters

When route inventory is unchanged, the risky work is not adding a URL. It is making sure both sides agree on the meaning of each route.

AreaContract requirement
Recording session identityOne recording session owns the MP4, input CSV, and metadata JSON artifacts. UI history groups by recording_session / recording_session_id, falling back to legacy session_id only when needed.
Terminal reasonsFailed or not-counted history rows must carry a stable reason (reason, error_message, or equivalent normalized field).
Selected game lifecycleThe selected game is the only game whose process/window should be monitored for recording. Switching games releases the previous monitor work.
Recorder status/api/recorder/status must describe the selected game lifecycle clearly enough for the island to move between idle, ready, recording, paused, and blocked states.
Environment warnings/api/environment/check warnings need stable codes so UI can suppress or group specific cases deterministically.
Localhost hardeningAll non-test UI-to-core calls should be rejected without the expected session-auth headers.
Upload historyHistory is a real read model from core/SQLite in normal mode; mock data is dev-preset only.

UI-consumed routes

These are the routes currently called by playroll-ui main-process IPC.

MethodRouteUI purposeContract notes
GET/api/statusGeneral core status fallback/read model.Must include enough nested recorder state for legacy fallback from /api/recorder/status. Carries a recorder.perf object (or null when idle/warmup) mirroring the FPS-profiler window — framesSeen/Encoded/Muxed/Dropped, dropNoPoolSlot, dropQueueFull, p95EncodeMs, packetQueueDepthCurrent, poolAvailableCount/poolSlotCount, plus severity (good/degraded/severe/critical) and reason. Powers the stats overlay and any UI perf readout.
GET/api/recorder/statusCurrent recorder/island/session state.Should expose selected game, detection state, recording/paused phase, timing, terminal UI error if present. EC-286: also includes micEnabled / micMuted so the recording island reflects the live F7 mute state (the mic button previously only tracked its own click-toggles).
POST/api/record/startStart recording for the selected game.Must enforce auth, selected-game constraints, start gates, and return stable block reasons.
POST/api/record/stopStop current recording.Should return success or stable denial such as not recording.
POST/api/record/pausePause current recording.Should return stable denial reasons when pause is not allowed.
POST/api/record/resumeResume paused recording.Should return stable denial reasons when resume is not allowed.
POST/api/overlay/toggleToggle recorder overlay visibility.Should return visible state plus optional UI error directive. Legacy native-overlay control — see the overlay convergence note on /api/overlay/stats.
POST/api/overlay/statsEnable/disable the live performance HUD (FPS, bitrate, dropped frames, encode latency, queue/pool pressure).Body { enabled: bool }{ success, enabled }. Core holds this as runtime-only state — the UI persists the showStatsOverlay preference and re-applies it on connect. The F8 hotkey toggles it at runtime; the resulting master state is reported as recorder.statsOverlayEnabled on /api/status (and /api/recorder/status) and as overlaysEnabled on /api/overlay/alt-held so the overlay reconciles instantly. F8 is the default but is now a configurable chord (toggle_overlay on /api/hotkeys, EC-475); it still registers best-effort and yields to the recording chords if it collides with one. Overlay convergence (now the default): the native Direct2D HUDs (stats / mic / GPU / preflight / game card) are suppressed and the cross-platform Electron overlay renders these widgets instead; F8 now shows/hides the whole Electron overlay (not just the stats HUD). Set PLAYROLL_LEGACY_OVERLAY=1 to fall back to the native HUDs (Windows only, for diagnosis).
GET/api/overlay/alt-heldFast overlay poll (~120 ms) that drives the Electron overlay.Auth-gated. Returns { held, mic?: { level, muted, enabled }, game?: { focused, rect: { x, y, width, height } }, overlaysEnabled }. held is the global Alt-key state (GetAsyncKeyState, focus-independent) powering the overlay's Alt-hold-to-edit gesture while a game is foreground; mic drives the smooth VU meter; game.rect/focused position the transparent host window over the game; overlaysEnabled mirrors the F8 master state for an instant show/hide. Deliberately tiny (atomic reads only) — polled only while the overlay is on screen. Note: per-game gameSlug and the GPU/perf figures are not here — they ride /api/status's recorder + recorder.perf.
GET/api/uploader/statusUploader state and pending files.Should include paused/uploading state, current file, progress, pending count, and last error when available.
GET/api/uploader/historyActivity/history rows.Must preserve row id and session identity separately. recording_session groups MP4 + CSV + JSON rows; session_id is legacy/internal-compatible. Failed rows need reason/error. EC-286: also returns rows with status held (manual-upload mode, kept on device) and per-row has_mic_audio / has_transcript flags (derived from the files on disk) so the UI gates the per-row "Transcribe" / "Review" actions.
POST/api/uploader/actionUploader command such as scan/pause/resume.Must return accepted action and stable failure reason.
GET / PUT/api/uploader/autoEC-286: read/write the auto-upload toggle.Backed by the auto_upload_enabled app setting. GET → { enabled }; PUT body { enabled }{ success, enabled }. Absent setting reads as true, so existing installs keep auto-uploading after upgrade. When false, finished recordings are held and transcription becomes on-demand.
POST/api/uploader/releaseEC-286: release held files into the upload pump.Body { sessionId? } — release one recording, or omit (or "*") to release all held files ("Upload all"). Flips held → pending and wakes the pump. Response { success, released }.
GET/api/environment/checkBlocking/warning environment checks.Warning/blocker codes must be stable; UI may suppress known codes like multi-GPU.
GET/api/environment/adapterHardware adapter snapshot (vendor, GPU name, NVENC driver).Used by the onboarding system-check step to render a precise driver-version prompt.
GET/api/hw/snapshotCanonical HW snapshot — system (CPU/RAM/OS) and gpus[] (vendor/device/driver/VRAM).Same payload the core writes into every recording's _meta.json::hardware. Consumed by the in-app bug-report flow; one schema across recording metadata, bug reports, and the device_hw_snapshots inventory.
GET/api/settingsRead core settings.Response should be tolerant to missing fields but stable for saved settings.
PUT/api/settingsUpdate core settings.Must merge without dropping unrelated settings.
GET/api/sound-cuesRead sound cue bindings.Snapshot should be deterministic and include enabled state per cue.
PUT/api/sound-cuesSave sound cue bindings.Must validate cue ids and file paths.
POST/api/sound-cues/previewPreview a sound cue.Must not persist state.
GET/api/audio/devicesEnumerate active mic (eCapture) endpoints.Each entry has id, name, isDefault, sampleRate, channels. The default-communications endpoint is flagged so the UI can mark it.
GET/api/audio/mic-configRead mic recorder configuration.Response shape: { deviceId, enabled, muted, aacBitrateBps }. deviceId empty string ⇒ default communications endpoint.
POST/api/audio/mic-configPartial-update mic recorder config.Any subset of the four fields can be sent. muted applies immediately; deviceId change applies on the next recording session start. EC-263: flipping enabled to true is gated by the privacy gate — rejected with HTTP 403 + { success: false, reason: "MIC_PRIVACY_GATE_BLOCKED", message, config } when the local HW can't run on-device transcription. The current core-side config is returned in config so the caller can re-sync.
GET/api/audio/mic-meterLive VU level for the settings UI.Short-poll JSON { level, running } with level peak-normalised to [0, 1]. UI polls at ~100 ms while the settings panel is open.
POST/api/audio/mic-testRecord a short test clip to a temp .aac.Body: { seconds } (1..30, default 5). Response: { success, path, seconds }. Force-enables the mic for the duration of the test even when enabled=false. Rejects with 409 if a real recording session is active.
GET/api/transcribe/privacy-gateCached EC-263 privacy-gate verdict.Response: { supported, reason, hw: { gpuVendor, gpuVramMb, totalRamMb } }. reason codes: OK, NO_NVIDIA_GPU, INSUFFICIENT_VRAM, INSUFFICIENT_RAM, NO_MODEL_FOR_TIER, REGISTRY_EMPTY, PRIVACY_GATE_UNAVAILABLE. The verdict is cached at cpp startup and is the source of truth for whether the mic toggle is allowed.
GET/api/transcribe/recommendedRecommended Whisper model for current HW.Response: { supported, modelId, backend, sizeBytes, tier, reason, hw }. supported=false for non-NVIDIA HW.
GET/api/transcribe/modelsFull bundled model catalog with per-entry install/download state.Each entry: { id, family, filename, url, sha256, sizeBytes, tier, requirements: { minVramMb, minRamMb }, metrics: { werEnPct, approxRtfCuda }, installed, progress? }.
POST/api/transcribe/downloadEnqueue a model download.Body: { modelId }. Response: { success, modelId, status } where status is started / already_queued / already_in_progress / already_installed. WinHTTP Range resume + sha256 verify + atomic .part → .bin promotion.
DELETE/api/transcribe/model/:idUninstall a previously-downloaded model.Removes the .bin + sidecar from disk; future downloads will re-fetch.
GET/api/transcribe/models-pathCurrent models folder.Response: { path, isDefault }. Default: %LOCALAPPDATA%\\Playroll\\models. Persisted in app_settings.transcription.models_path.
POST/api/transcribe/models-pathChange the models folder.Body: { path, moveExisting }. Cross-volume copy+delete fallback when moveExisting=true. Response: { success, outcome: { previousPath, newPath, movedCount, skippedCount, errors[] } }.
GET/api/transcribe/jobsRecent transcription queue snapshot (EC-264).Response: { jobs: [{ id, sessionId, micPath, transcriptPath, modelId, status, attempts, enqueuedAtMs, startedAtMs, finishedAtMs, errorMessage, progressPercent }] }. Ordered DESC by enqueuedAtMs. statuspending/in_progress/done/failed. Consumed by the launcher footer's TranscriptionStatusBadge (EC-265). EC-286: progressPercent now advances during inference (whisper decode progress mapped into the 20→90 band) instead of stalling at the post-decode value.
POST/api/transcribe/runEC-286: on-demand transcription for one session (manual-upload mode).Body { sessionId } (the sessions.id PK). Resolves the session's mp4/_mic.aac and enqueues a job (upsert on session_id, so it is re-runnable after a prior done/failed). Response { sessionId, jobId }; errors: SESSION_NOT_FOUND / MP4_NOT_FOUND / MIC_AUDIO_NOT_FOUND / ENQUEUE_FAILED.
POST/api/transcribe/fileEC-317: synchronous onboarding self-test — transcribe an arbitrary audio file (the mic-test clip) and return the transcript inline.Body { path, language? } (language empty / omitted = auto-detect). Resolves the recommended installed model, runs the isolated child process (so a CUDA/ggml abort can't take down core), and blocks for the short inference. Success: { success: true, transcript: { schema_version, language, duration_seconds, segments: [{ id, start, end, text, confidence }], text, … } }. Failure: HTTP 422 { error, errorCode? } (errorCodeMODEL_NOT_INSTALLED / INSUFFICIENT_VRAM / …). Unlike /run, this does not touch the SQLite job queue and uses a synthetic session_id of "selftest".
POST/api/lobby/setMirror the active multi-POV lobby on core.Body: { lobby_id, lobby_code, game_slug? }. Regex-validates UUID / 6-char [A-Z0-9] / kebab-case slug. See Lobby Mode.
POST/api/lobby/clearDrop the active lobby on core.No body.
GET/api/lobby/statusRead the active lobby.Always 4 keys; identifier fields are empty strings when active=false.
GET/api/encodersRead available encoders.Should include selected encoder and unavailable reasons.
POST/api/encoders/selectSelect encoder.Must reject unavailable encoders with stable reason.
GET/api/monitor/targetsRead game/process monitor targets.Should reflect installed games known to core and selected-game constraints.
PUT/api/monitor/targetsReplace/sync monitor targets.Must be machine-independent and avoid hardcoded local paths.
POST/api/monitor/rescanForce monitor target rescan.Should be idempotent and safe while UI is open.
PUT/api/monitor/selectionSet selected game.Switching selection must release monitors for the previous selected game.
POST/api/settings-enforcement/applyApply per-game settings enforcement.Should scope to the selected game/store id.
POST/api/settings-enforcement/restoreRestore settings after a session.Must be safe to call if nothing was applied.
GET/api/settings-enforcement/statusRead enforcement status, optionally per game.Supports query by game_store_id.
PUT/api/settings-enforcement/sync-targetsSync settings-enforcement targets.Must stay aligned with monitor targets and selected game.
POST/api/debug/toastDev/debug notification cycle.Debug route; should remain non-player behavior.
POST/api/debug/overlay/cycleDev/debug overlay cycle.Debug route; should remain non-player behavior.

Full core route inventory

These routes are exposed by playroll-cpp on the integration branch.

Runtime and docs

MethodRouteNotes
GET/healthHealth probe.
GET/pingLightweight liveness probe.
POST/heartbeatUI/core heartbeat.
POST/shutdownCore shutdown command.
GET/api/docsBuilt-in core API documentation page.

Auth

MethodRouteNotes
POST/auth/tokenSupplies auth token to core.
POST/api/authAuth-related API route exposed by core.

Recorder and overlay

MethodRouteNotes
GET/api/statusGeneral status envelope.
GET/api/recorder/statusRecorder-specific status.
POST/api/record/startStart recording.
POST/api/record/stopStop recording.
POST/api/record/pausePause recording.
POST/api/record/resumeResume recording.
POST/api/overlay/toggleToggle overlay visibility (legacy native path).
POST/api/overlay/statsEnable/disable the live performance HUD (now rendered by the Electron overlay; native HUDs suppressed unless PLAYROLL_LEGACY_OVERLAY=1).
GET/api/overlay/alt-heldFast overlay poll (~120 ms): Alt-held + mic level + game rect/focus + overlaysEnabled (F8 state).

Game library, monitor targets, and selection

MethodRouteNotes
GET/api/gamesCore game list.
POST/api/games/refreshRefresh known games.
POST/api/games/rescanRescan games.
GET/api/monitor/targetsRead monitor targets.
PUT/api/monitor/targetsReplace monitor targets.
POST/api/monitor/rescanRescan monitor targets.
PUT/api/monitor/selectionSet selected game for process/window monitoring.

Settings and enforcement

MethodRouteNotes
GET/api/settingsRead settings.
PUT/api/settingsUpdate settings.
GET/api/settings-enforcement/statusRead enforcement status.
POST/api/settings-enforcement/applyApply enforced settings.
POST/api/settings-enforcement/restoreRestore enforced settings.
PUT/api/settings-enforcement/sync-targetsSync enforcement targets.
GET/api/settings/audio-cuesLegacy audio cue settings route.
PUT/api/settings/audio-cuesLegacy audio cue settings update route.
GET/api/sound-cuesRead sound cue snapshot.
PUT/api/sound-cuesUpdate sound cue snapshot.
POST/api/sound-cues/previewPreview a sound cue.
GET/api/hotkeysRead hotkey settings.
PUT/api/hotkeysUpdate hotkey settings. Fields: start_recording, stop_recording, mute_mic, toggle_overlay (all rebindable; toggle_overlay added EC-475, default F8).
GET/api/preprocessor/configRead preprocessor config.
PUT/api/preprocessor/configUpdate preprocessor config.

Audio / microphone (EC-259)

The microphone capture path is independent of the per-process loopback used for game audio. Its output is a sidecar {baseName}_mic.aac file written alongside the .mp4, with the same duration contract (±1 frame). Mute toggles inject silent PCM so the duration stays aligned across arbitrary user toggling. The recorder is opt-in (enabled=false by default) for privacy; see Privacy → data collection.

MethodRouteNotes
GET/api/audio/devicesList active eCapture endpoints. Response: { devices: [{ id, name, isDefault, sampleRate, channels }] }.
GET/api/audio/mic-configRead mic recorder config. Response: { deviceId, enabled, muted, aacBitrateBps }.
POST/api/audio/mic-configPartial-update mic recorder config. Body: any subset of { deviceId, enabled, muted, aacBitrateBps }.
GET/api/audio/mic-meterVU level snapshot. Response: { level, running } with level ∈ [0, 1] peak-normalised.
POST/api/audio/mic-testRecord a short test clip. Body: { seconds }. Response: { success, path, seconds }. Rejects with 409 while a real recording session is active.

Transcription (EC-260 / EC-261 / EC-262 / EC-263)

On-device transcription via whisper.cpp on CUDA. The mic toggle in /api/audio/mic-config is coupled to this pipeline by the privacy gate (see Privacy → On-device transcription and the mic privacy gate).

MethodRouteNotes
GET/api/transcribe/privacy-gateCached EC-263 verdict: { supported, reason, hw: { gpuVendor, gpuVramMb, totalRamMb } }.
GET/api/transcribe/recommendedRecommended Whisper model for current HW: { supported, modelId, backend, sizeBytes, tier, reason, hw }.
GET/api/transcribe/jobsQueue snapshot (EC-264): { jobs: [{ id, sessionId, status, progressPercent, … }] }.
GET/api/transcribe/modelsFull bundled model catalog with per-entry install/download state.
POST/api/transcribe/downloadEnqueue a model download. Body: { modelId }.
POST/api/transcribe/fileEC-317: synchronous self-test — transcribe a file ({ path, language? }) in the isolated child, transcript returned inline. Onboarding mic-test confirmation.
DELETE/api/transcribe/model/:idUninstall a downloaded model.
GET/api/transcribe/models-pathCurrent models folder: { path, isDefault }.
POST/api/transcribe/models-pathChange the models folder. Body: { path, moveExisting }.

Lobby (multi-POV grouping)

The lobby is an optional in-memory grouping mechanism that decorates simultaneous recording sessions with a shared lobby_id / lobby_code so MP4s from multiple players can be aligned in post-production. It is never the identity of a session — see Architecture → Lobby Mode for the full picture, validation rules, and recording-start interaction.

MethodRouteNotes
POST/api/lobby/setSet the active lobby. Body: { lobby_id, lobby_code, game_slug? }. lobby_id must be a UUID; lobby_code must match [A-Z0-9]{6}; game_slug, if present, must be lowercase kebab-case (≤ 128 B). Rejects with 400 + field-specific message on mismatch.
POST/api/lobby/clearDrop the active lobby.
GET/api/lobby/statusRead the active lobby. Always returns { active, lobbyId, lobbyCode, gameSlug }. When active=false, identifier fields are empty strings (no leak of the previous lobby's values).

Uploader and validation read models

MethodRouteNotes
GET/api/uploader/statusRead uploader status.
GET/api/uploader/historyRead recent uploaded/failed history rows.
POST/api/uploader/actionControl uploader.
GET/api/uploader/configRead uploader config.
PUT/api/uploader/configUpdate uploader config.
GET/api/environment/checkRead environment blockers and warnings.
GET/api/environment/adapterRead primary HW adapter snapshot (vendor + driver versions).
GET/api/hw/snapshotRead canonical HW snapshot (same shape as _meta.json::hardware).