Local API
What the Local API does
Audio Forge includes a small built-in server that runs on your machine. It lets external tools, like an Elgato Stream Deck plugin, a custom script, or any app on your PC, control playback over HTTP or WebSocket, with no extra software required.
If you already use the MQTT integration, the Local API supports the exact same commands. The difference is that it runs entirely on your machine with zero setup: no broker to install, no network to configure.
Enabling the Local API
Go to Settings → External Control → Local API and make sure the feature is enabled (it’s on by default).
You can configure:
- Enabled toggle (default: on).
- Port number (default: 8329).
- Listen on LAN toggle (default: off). When on, devices on your local network can reach the API. When off, only software on the same machine can connect.
- API Key (auto-generated). Required when connecting from another device on the network. You can copy it or regenerate it from Settings.
Once enabled, Audio Forge listens on http://localhost:8329. If “Listen on LAN” is turned on, it listens on all network interfaces instead.
Quick start
Open a terminal and try:
# See what's playing right now
curl http://localhost:8329/api/state
# List every library and category
curl http://localhost:8329/api/catalog
# List one-shot Echo sounds that are playing now
curl http://localhost:8329/api/echoes/active
# Play a category
curl -X POST http://localhost:8329/api/command ^
-H "Content-Type: application/json" ^
-d "{\"command\":\"play\",\"section\":\"Music\",\"categoryName\":\"Battle\"}"
That’s it, if Audio Forge is running with the Local API enabled, you’ll get JSON responses immediately.
HTTP endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/state | Current playback state |
| GET | /api/catalog | Full library and category catalog |
| GET | /api/icon/{categoryUuid} | Render a category icon as image data |
| GET | /api/echoes/active | Currently playing one-shot Echo sounds |
| GET | /api/info | API version and available features (see Detecting features) |
| GET | /api/ai_director/status | AI Director status (see AI Director) |
| POST | /api/command | Execute a command (JSON body) |
GET /api/state
Returns a JSON snapshot of what Audio Forge is doing right now:
{
"libraryUuid": "...",
"libraryName": "[Default]",
"musicPlaying": true,
"ambiancePlaying": false,
"musicVolume": 0.75,
"ambianceVolume": 0.5,
"echoesVolume": 1.0,
"musicCategories": [{"uuid": "...", "name": "Battle"}],
"ambianceCategories": [{"uuid": "...", "name": "Rain"}],
"lastEcho": {"uuid": "...", "name": "Thunder", "at": "2025-06-01T12:34:56Z"},
"categoryVolume": {"<uuid>": 0.5}
}
All volume fields use the app’s display scale from 0.0 to 1.0.
For Music and Ambiance, 0.5 is normal (unity) volume and 1.0 is the
boosted maximum. For Echoes, 1.0 is normal volume.
categoryVolume contains current runtime values and may be sparse. Use each
category’s volume field from /api/catalog when you need a complete value
for every category, including categories not yet used.
GET /api/catalog
Returns every library with its Music and Ambiance categories:
{
"activeLibraryUuid": "...",
"libraries": [
{
"uuid": "...",
"name": "[Default]",
"music": [{
"uuid": "...",
"name": "Battle",
"enabledInForge": true,
"trackCount": 3,
"volume": 0.5
}],
"ambiance": [{
"uuid": "...",
"name": "Rain",
"enabledInForge": false,
"trackCount": 0,
"volume": 0.5
}]
}
]
}
enabledInForgesays whether the category is visible and enabled in the Forge.trackCountis the number of playable tracks currently installed for that category.volumeis the category’s effective volume on the0.0to1.0display scale. It is present for every category, including inactive categories.
For an active category, the catalog reports its current runtime volume. For an
inactive category, it reports the configured target volume, or 0.5 when no
target exists. Runtime adjustments do not replace the configured target.
When a category starts again, an explicit command value is used when present;
otherwise the configured target is used. The previous runtime adjustment is
not reused. Restoring a State Link applies the runtime volumes recorded in that
link.
GET /api/echoes/active
Returns a sampled snapshot of every one-shot Echo sound that is currently playing. Simultaneous triggers are returned as separate entries, even when they use the same category or sound file.
{
"count": 1,
"sampledAt": "2026-07-24T12:00:02.500Z",
"echoes": [
{
"playbackId": "7b304347-410f-4eaf-bad7-4e8488778302",
"categoryUuid": "2e4d51f1-11ce-46af-a411-0c6bc2c420cb",
"categoryName": "Thunder",
"fileName": "thunder-01.wav",
"startedAt": "2026-07-24T12:00:00.000Z",
"durationMs": 10000,
"positionMs": 2500,
"remainingMs": 7500,
"progress": 0.25,
"progressPercent": 25.0,
"playing": true,
"processingState": "ready"
}
]
}
playbackIduniquely identifies this specific trigger. Two overlapping plays of the same category receive different IDs.sampledAtis when Audio Forge generated the snapshot. Position values are not streamed continuously, so clients should request a new snapshot when they need current progress.durationMs,remainingMs,progress, andprogressPercentarenullwhen the sound’s duration is unavailable.progressuses a0.0to1.0scale.progressPercentuses0.0to100.0.fileNamecontains only the file name, never a local filesystem path.- Finished or stopped Echo sounds are removed from the list.
Echoes share the global echoesVolume value returned by /api/state. There
is no per-playback or per-category Echo volume.
POST /api/command
Send a JSON command in the request body. On success:
{"ok": true, "message": "play ok"}
On error:
{"ok": false, "error": "category not found"}
GET /api/icon/{categoryUuid}
Returns the image for a category icon. Use the category UUID from /api/catalog.
The category must belong to an active library. The optional size query parameter
sets the requested size for font and SVG icons and defaults to 144 pixels.
- Font icons and SVG icons are rendered as PNG images and tinted with the category color.
- If a custom file is missing or an SVG cannot be rasterized, the API falls back to the font icon.
- Custom PNG, JPG, and JPEG icons are returned unchanged; the
sizeparameter is ignored. - Successful responses use
image/pngorimage/jpegand may be cached for one hour. - If the category is not found, the response is
404with{"error":"category not found"}. - If the icon cannot be rendered, the response is
500with{"error":"failed to render icon"}.
WebSocket
For tools that want real-time updates (like a Stream Deck plugin that needs to show current state), connect a WebSocket to:
ws://localhost:8329/ws
Sending commands
Send the same JSON command objects you would POST to /api/command:
{"command": "play", "section": "Music", "categoryName": "Battle"}
The server replies with a result message for each command.
Receiving updates
The server automatically pushes updates whenever something changes. Each message has a type field:
| type | When it’s sent | What it contains |
|---|---|---|
state | On connect + whenever playback changes | Same as GET /api/state |
catalog | On connect + when libraries change | Same as GET /api/catalog |
result | After each command you send | ok, message or error, requestId |
echo | When an echo sound is triggered | uuid, name, at |
hello | On connect, first message | apiVersion and features, see Detecting features |
ai_director | On connect + when the AI Director changes | Same as GET /api/ai_director/status |
ai_director_log | On connect + as the AI Director hears and acts | Recent activity entries |
State updates are debounced (250 ms) so you won’t be flooded during rapid changes like volume sweeps.
Messages are flat JSON objects. The payload is not nested under a data or
payload field. Examples:
{"type": "state", "musicPlaying": true, "ambiancePlaying": false,
"musicVolume": 0.5, "ambianceVolume": 0.5,
"musicCategories": [], "ambianceCategories": [], "categoryVolume": {}}
{"type": "catalog", "activeLibraryUuid": "<uuid>", "libraries": []}
{"type": "result", "ok": true, "message": "play ok", "requestId": "request-1"}
{"type": "echo", "uuid": "<uuid>", "name": "Thunder",
"at": "2026-07-18T12:34:56Z"}
Commands
All commands use the same JSON schema as the MQTT integration. Every command accepts an optional requestId string you can use to match responses.
play
Play or resume a section, or select a specific category.
{"command": "play", "section": "Music"}
{"command": "play", "section": "Music", "categoryName": "Battle"}
{"command": "play", "section": "Ambiance", "categoryUuid": "<uuid>"}
- Without a category: resumes the whole section.
- With a category: selects and plays it.
- Ambiance uses toggle semantics. Playing an already-active Ambiance category turns it off.
pause
{"command": "pause", "section": "Music"}
stop
{"command": "stop", "section": "Ambiance"}
setActiveLibrary
Switch the active library by UUID or name.
{"command": "setActiveLibrary", "libraryName": "My Library"}
{"command": "setActiveLibrary", "libraryUuid": "<uuid>"}
setVolume
Set the volume for a whole section, or for a specific category within a section. Use value from 0.0 to 1.0, or from 0 to 100 with valueScale: percent.
{"command": "setVolume", "section": "Music", "value": 1.0}
{"command": "setVolume", "section": "Music", "value": 75, "valueScale": "percent"}
{"command": "setVolume", "section": "Echoes", "value": 0.5}
{"command": "setVolume", "section": "Ambiance", "value": 0.5,
"categoryName": "Rain", "transitionMs": 1000}
The optional transitionMs controls how quickly the volume fades to the new level (default: 500 ms).
Music and Ambiance use the same display scale as the app: 0.5 is normal
(unity) volume and 1.0 is the boosted maximum. Echoes do not have a boosted
range, so 1.0 is normal volume. The API converts these display values to the
internal playback gain automatically.
nextTrack
Skip to the next track in an active Music or Ambiance category. The category must already be playing.
{"command": "nextTrack", "section": "Music", "categoryUuid": "<uuid>"}
{"command": "nextTrack", "section": "Ambiance", "categoryName": "Rain"}
playEcho
Trigger a one-shot Echo sound.
{"command": "playEcho", "categoryName": "Thunder"}
{"command": "playEcho", "section": "Ambiance", "categoryUuid": "<uuid>"}
On success, the command waits until the Echo playback is registered and returns its tracking and playback fields:
{
"ok": true,
"message": "echo played",
"requestId": "play-thunder",
"playbackId": "<playback-id>",
"categoryUuid": "<category-uuid>",
"categoryName": "Thunder",
"fileName": "thunder-01.wav",
"startedAt": "2026-08-09T12:00:00.000Z",
"durationMs": 4000,
"positionMs": 0,
"remainingMs": 4000,
"progress": 0.0,
"progressPercent": 0.0,
"playing": true,
"processingState": "ready"
}
Use the returned playbackId with stopEcho to stop only this playback
instance. Current values can also be refreshed with GET /api/echoes/active.
stopEcho
Stop active Echo playback using exactly one selector:
{"command": "stopEcho", "playbackId": "<playback-id>"}
{"command": "stopEcho", "categoryUuid": "<category-uuid>"}
{"command": "stopEcho", "categoryName": "Thunder"}
playbackIdstops only that playback instance. Get playback IDs fromGET /api/echoes/active.categoryUuidstops every active Echo from that category.categoryNamestops every active Echo whose category has that exact name. If categories in multiple libraries share the name, all matches are stopped.- The result includes
stoppedCount. A selector with no active matches succeeds withstoppedCount: 0. - To stop every active Echo regardless of category, use
{"command": "stop", "section": "Echoes"}.
setCategoryEnabled
Explicitly enable or disable a category in a section.
{"command": "setCategoryEnabled", "section": "Ambiance",
"categoryName": "Rain", "enabled": true}
You can enable a category and set its volume atomically by including value.
If value is omitted, the category’s configured target volume is used. A
previous runtime adjustment is not reused.
{"command": "setCategoryEnabled", "section": "Ambiance",
"categoryName": "Rain", "enabled": true, "value": 0.5}
restoreState
Restore a previously saved state from a share link created by the app.
{"command": "restoreState", "link": "slashpaf://..."}
setConfig
Forward a configuration setting to the MQTT integration (if connected). Useful for advanced automation.
{"command": "setConfig", "key": "someKey", "value": "someValue"}
Detecting features
Newer versions of Audio Forge add features to the API. So a tool can tell what the running app supports, the server announces its version. It’s the first WebSocket message, and GET /api/info returns the same thing:
{"type": "hello", "apiVersion": 3,
"features": {"aiDirector": {"available": true, "enabled": true},
"roomControl": {"commands": ["play", "pause", "stop", "..."]}}}
| Field | Meaning |
|---|---|
apiVersion | Starts at 1. Version 2 (Audio Forge 1.5.18) added the AI Director. Version 3 added room control. |
features.aiDirector.available | This platform supports the AI Director. |
features.aiDirector.enabled | The AI Director is switched on in this app’s settings. |
features.roomControl.commands | The commands a room guest can be allowed to run. |
Older versions of Audio Forge send no hello message and have no /api/info: treat that as apiVersion 1 and don’t call anything newer. Only show or call AI Director features when available and enabled are both true.
Room control
When you host an online room you can let guests control Audio Forge from the guest page. Two commands set who can (they only work while a room is open):
{"command": "setRoomDefaultPermission", "permission": "control"}
{"command": "setRoomGuestPermission", "clientId": "<guest clientId>", "permission": "listen"}
permission is "listen" (only you control Audio Forge) or "control". The room status in GET /api/state (room) then includes defaultGuestPermission and a permission for each entry of guests (the effective one: with "control" as the default you can still set a single guest to "listen"). room.chatMessages also lists what controlling guests did, as entries with kind: "activity", command and resolved params; chat entries have kind: "chat". Guests joining or leaving appear as activity entries with command guestJoined, guestLeft, guestRemoved or guestReconnected. While guests can control, Audio Forge also uploads the category icons to the rooms server so the guest page can show them. Guests with control can only run the commands listed in features.roomControl.commands; capturing, removing guests, configuration, the AI Director and permissions stay with you.
AI Director
The AI Director listens to your table and changes the music and ambiance for you. Through the Local API you can start and stop it, choose where it gets its audio, feed it text, and follow what it understood and did. It needs the AI Director to be enabled in the app and its models to be installed.
Commands
| Command | Fields | What it does |
|---|---|---|
aiDirectorStart | inputSource (optional), fresh (optional), extraReasoning (optional), device (optional), gpuLayers (optional), threads (optional), probe (optional), whisperGpu (optional) | Start listening. Loading the models takes a while, so the reply comes first: watch state go from activating to listening. fresh: true forgets the previous scene. extraReasoning overrides the app’s Extra reasoning setting for this session only: true asks the model a second, short question to find the sound effect, which is more accurate and a little slower. device (Android only, for testing) is where the model runs: gpu, cpu or auto; the model reloads when it changes. gpuLayers and threads (Android only, for testing, GGUF models only) set the GPU layers and the CPU thread count; 0, the default, keeps the model on the CPU and lets the engine choose the threads. probe: true (for tuning) logs main-thread stalls and slow frames, with build and raster time apart, into the debug log for this session. whisperGpu (for testing) runs speech recognition on the GPU (true) or the CPU (false) for this session only; it is on by default; on Android it is only used on Adreno GPUs. Refused with an error if the feature is off, not unlocked, or the models aren’t installed. |
aiDirectorStop | Stop listening and free the models. | |
aiDirectorSetInput | inputSource | Switch where the audio comes from while running. |
aiDirectorSendText | text | Give the AI Director text as if it had been heard, with no microphone and no speech recognition. See below. |
inputSource is "device" (the input device chosen in Audio Forge, the default) or "remote" (audio sent by your tool over the WebSocket).
{"command": "aiDirectorStart", "inputSource": "remote", "fresh": true}
Sending text
aiDirectorSendText is fire and forget: the reply only confirms the text was queued. Like with speech, the AI Director gathers what it hears for a few seconds and then decides what, if anything, to change. It is refused unless the AI Director is running.
{"command": "aiDirectorSendText", "text": "Heavy rain starts to fall on the old market square."}
This is useful for tools that already have a transcript, for text-based sessions, and for testing how the AI Director reacts to a scene.
Status
GET /api/ai_director/status, and the ai_director WebSocket message, which is pushed whenever something changes:
{"type": "ai_director", "available": true, "enabled": true, "entitled": true,
"modelsReady": true, "state": "listening", "inputSource": "device",
"sceneDescription": "Heavy rain over a market square", "lastTranscript": "...", "error": null}
| Field | Meaning |
|---|---|
entitled | The AI Director is unlocked on this account. |
modelsReady | The speech and language models are installed. |
state | idle, activating, listening, processing or paused. |
inputSource | device or remote. |
extraReasoning | Whether Extra reasoning is on for this run. |
backend | The inference backend the language model loaded with (for example Vulkan or CPU), or null while it is not loaded. |
whisperGpu | Whether speech recognition runs on the GPU (Vulkan) when the build has one; it falls back to the CPU by itself when there is no usable GPU. |
whisperBackend | The compute devices the speech recognition library can use, for example CPU; Vulkan0 Radeon 8060S Graphics (gpu), or null before the first start. A build without the GPU backend lists only the CPU. |
sceneDescription, lastTranscript | What the AI Director currently believes, and the last thing it heard. |
error | The reason it couldn’t start, if any. |
Activity log
The ai_director_log WebSocket message carries what the AI Director heard and did. It is sent in full when you connect (reset: true, the last 100 entries), then only new entries:
{"type": "ai_director_log", "reset": false,
"entries": [{"id": 42, "at": "2026-10-27T15:20:48Z", "kind": "transcript", "text": "..."}]}
kind is transcript (what was heard), action (what it changed), error or info.
For a deeper look, GET /api/ai_director/debug?since=<id> returns every entry after id, including the prompt sent to the language model and its raw answer. It is meant for tuning, not for display.
Sending your own microphone audio
With inputSource set to "remote", send binary WebSocket frames of raw audio: 16 kHz, mono, signed 16-bit little-endian PCM. Audio Forge applies its usual silence detection, so you can stream continuously. Frames are ignored while the source is "device". Only one WebSocket client can supply audio at a time; when it disconnects, the AI Director goes back to the input device.
Error handling
If a command can’t be executed (for example, a category name doesn’t exist or a required field is missing), the response will contain "ok": false and an error message describing what went wrong.
{"ok": false, "error": "category not found"}
{"ok": false, "error": "section required"}
{"ok": false, "error": "invalid json"}
Security
By default the Local API binds to 127.0.0.1 (localhost only) and is not reachable from other devices on your network.
If you enable Listen on LAN, the API becomes reachable from other devices. In that case an API key is required for all non-localhost requests. The key is auto-generated and shown in Settings → External Control → Local API; pass it with every request using one of:
- Header:
Authorization: Bearer <your-api-key> - Query parameter:
?apiKey=<your-api-key>
Examples:
# Using the Authorization header
curl http://192.168.0.10:8329/api/state ^
-H "Authorization: Bearer your-api-key-here"
# Using the query parameter
curl "http://192.168.0.10:8329/api/state?apiKey=your-api-key-here"
# Sending a command from another device
curl -X POST http://192.168.0.10:8329/api/command ^
-H "Authorization: Bearer your-api-key-here" ^
-H "Content-Type: application/json" ^
-d "{\"command\":\"play\",\"section\":\"Music\",\"categoryName\":\"Battle\"}"
# WebSocket with API key
wscat -c "ws://192.168.0.10:8329/ws?apiKey=your-api-key-here"
Localhost requests never require a key, even when LAN mode is on.
You can regenerate the API key at any time from Settings → External Control → Local API. Any previously connected clients will need the new key.
Mobile background behavior
Mobile operating systems can suspend inbound HTTP and WebSocket handling when Audio Forge is backgrounded or the device is locked, even when native audio playback continues. The Local API does not deliberately stop itself, but it cannot guarantee background availability on iOS or Android.
For stable remote control during a session, keep Audio Forge in the foreground and keep the screen awake. This avoids relying on battery-intensive background execution that mobile platforms may stop at any time.