Skip to content

API Reference

All endpoints are served by the Express server on port 3456. The Vite dev server proxies /api requests.

All endpoints return errors as JSON with an error field:

{ "error": "description of the problem" }

Common HTTP status codes:

StatusMeaning
400Missing or invalid parameter (e.g., no soundId or level out of range)
404Sound ID not found in catalog
409Sound is already playing (duplicate layer), or MAX_LAYERS (8) reached
429Rate limit exceeded (120 mutations per 10 seconds)
500Engine or runtime error

Mutation endpoints (POST, DELETE) are rate-limited to 120 requests per 10-second window per IP. If the limit is exceeded the server returns 429 with { "error": "rate limit exceeded" }. Read-only endpoints (GET) are not rate-limited.

At most 8 layers can be active simultaneously. Adding a ninth layer returns 409.


Returns server health and runtime status.

{
"status": "ok",
"backendMode": "sonic-runtime"
}

backendMode is "sonic-runtime" when the NativeAOT engine is connected, or "mock" when running in fallback mode.


Returns the full sound catalog.

{
"categories": ["Rain", "Water", "Ocean", ...],
"sounds": [
{ "id": "heavy-rain", "name": "Heavy Rain", "category": "Rain" },
...
],
"grouped": {
"Rain": [{ "id": "heavy-rain", "name": "Heavy Rain", "category": "Rain" }, ...],
...
}
}

Hot-reloads the custom sounds directory (re-scans files and re-reads _meta.json). No request body needed. Returns the updated catalog.

{
"categories": ["Rain", ..., "Custom"],
"sounds": [...],
"grouped": { ... }
}

Use this after adding or removing files from the custom directory without restarting the server.


Returns available audio output devices.

[
{ "device_id": "default", "name": "Speakers (Realtek)", "is_default": true },
{ "device_id": "hdmi-1", "name": "HDMI Audio", "is_default": false }
]

Set the active audio output device and re-route all currently playing layers to it.

{ "deviceId": "hdmi-1" }

Returns: { "ok": true }

All active layers are moved to the new device immediately. New layers added after this call also use the selected device.


Returns current mixer state.

{
"layers": [
{ "soundId": "heavy-rain", "playbackId": "abc123", "volume": 0.7 },
{ "soundId": "fireplace", "playbackId": "def456", "volume": 0.4 }
],
"deviceId": "default",
"masterVolume": 1.0,
"timer": null,
"error": null
}

timer is null when no timer is active, or an object { "endsAt": "<ISO timestamp>", "remainingMs": 1740000 } when a timer is running.

masterVolume is 0.0–1.0 (default 1.0).


Add a new sound layer.

{ "soundId": "heavy-rain", "volume": 0.5, "fadeMs": 1000 }

fadeMs is optional. When provided, the layer fades in from silence to the target volume over the specified duration in milliseconds.

Returns: { "playbackId": "abc123" }

The server loads the WAV asset, starts looping playback via sonic-core, and adds the layer to state.


Remove a layer and stop its playback.

{ "playbackId": "abc123", "fadeMs": 800 }

fadeMs is optional. When provided, the layer fades out over the specified duration before stopping.


Set a layer’s volume.

{ "playbackId": "abc123", "level": 0.7 }

Level is 0.0–1.0.


Set the master (global) volume. Scales the output of all layers simultaneously.

{ "level": 0.8 }

Level is 0.0–1.0. Returns: { "ok": true }


Stop all active layers. No request body needed.


Get the current timer state.

{
"active": true,
"endsAt": "2026-03-29T23:30:00.000Z",
"remainingMs": 1740000
}

Returns { "active": false } when no timer is running.


Start a sleep timer. When the timer expires, all layers stop automatically.

{ "durationMs": 1800000 }

Common durations: 900000 (15m), 1800000 (30m), 3600000 (1h), 7200000 (2h).

Returns: { "endsAt": "<ISO timestamp>" }

Starting a new timer while one is active replaces the existing timer.


Cancel the active timer. The mix keeps playing. Returns { "ok": true }.


List all saved presets.

[
{
"id": "evening-wind-down",
"name": "Evening Wind Down",
"layers": [
{ "soundId": "heavy-rain", "volume": 0.6 },
{ "soundId": "fireplace", "volume": 0.3 }
],
"createdAt": "2026-03-28T20:00:00.000Z"
}
]

Save the current mix as a named preset.

{ "name": "Evening Wind Down" }

Returns the new preset object including its generated id.


Load a saved preset. The current mix is stopped and replaced with the preset’s layers.

No request body needed. Returns: { "ok": true }


Delete a saved preset. Returns: { "ok": true }


SSE endpoint. Sends data: messages with full MixerState JSON on every state change (default event type, no named event: field). First message is the current state on connect.


VariableDefaultDescription
SONIC_RUNTIME_PATH(fallback paths)Path to sonic-runtime binary
AMBIENT_WAVS_PATH./ambient-wavsDirectory containing ambient WAV files
STILLPOINT_CUSTOM_PATH<AMBIENT_WAVS_PATH>/../customDirectory for user-provided custom WAV files
STILLPOINT_DATA_PATH./dataDirectory where presets and other persistent data are stored
STILLPOINT_CORS_ORIGINShttp://localhost:5177Comma-separated list of allowed CORS origins
PORT3456Server port
VITE_API_BASEhttp://localhost:3456API base URL used by the Vite UI (set at build time)

By default the server only accepts requests from http://localhost:5177. To allow additional origins (for example, a remote dashboard or a second UI port), set this variable to a comma-separated list:

Terminal window
STILLPOINT_CORS_ORIGINS=http://localhost:5177,https://my-stillpoint.example.com

The UI uses VITE_API_BASE to know where the server lives. Override it if the server is not on localhost or is running on a non-standard port:

Terminal window
VITE_API_BASE=http://192.168.1.100:3456 npm run build --workspace=@stillpoint/ui

Each sound references a WAV file at ${AMBIENT_WAVS_PATH}/${sound.id}.wav:

  • Format: WAV (PCM)
  • Sample rate: 44,100 Hz
  • Channels: Mono
  • Duration: 60 seconds
  • Loop-friendly: designed for seamless repetition