obs-relay API Reference
Complete endpoint documentation for remote OBS control — scenes, transitions, playlists, audio, streaming, WebSocket relay, and TouchOSC bridge.
System
Connection health and version info
GET
/health
OBS connection status, active WebSocket clients, OSC bridge state
curl http://localhost:8080/health
Response
{
"status": "ok",
"obs_connected": true,
"ws_clients": 2,
"osc_active": true
}
GET
/obs/version
OBS Studio version, WebSocket protocol version, platform
curl http://localhost:8080/obs/version
Response
{
"obs_version": "30.1.2",
"obs_web_socket_version": "5.4.2",
"platform": "macos"
}
Scenes
Direct OBS scene control. Scene names must match exactly what's in OBS.
Scene names are case-sensitive and must match your OBS scene names exactly. Use
GET /obs/scenes to see the exact names OBS has registered.
GET
/obs/scenes
List all scenes in OBS with their index order
curl http://localhost:8080/obs/scenes
Response
[
{ "name": "Live", "index": 0 },
{ "name": "BRB", "index": 1 },
{ "name": "Standby", "index": 2 },
{ "name": "Intermission", "index": 3 },
{ "name": "EndCard", "index": 4 }
]
GET
/obs/scene/current
What scene is currently on program output
curl http://localhost:8080/obs/scene/current
Response
{ "scene": "Live" }
POST
/obs/scene/{scene_name}
Immediately switch to the named scene using OBS's active transition
The fade/transition effect comes from OBS, not this API. Set your transition type and duration in OBS (Scene Transitions panel) — obs-relay just triggers it. A 500ms fade is a good default.
| Parameter | Type | Required | Description |
|---|---|---|---|
| scene_name | string | required | Exact OBS scene name (URL path segment) |
# Switch to your live scene curl -X POST http://localhost:8080/obs/scene/Live # Switch to BRB screen curl -X POST http://localhost:8080/obs/scene/BRB # Scene names with spaces need URL encoding curl -X POST http://localhost:8080/obs/scene/My%20Scene
Response
{ "scene": "Live", "status": "ok" }
POST
/obs/studio/enable
Enable or disable Studio Mode (preview → program workflow)
Studio Mode gives you a preview scene you can set up privately, then cut to program when ready. Great for pre-loading the next video segment without the audience seeing it.
| Parameter | Type | Default | Description |
|---|---|---|---|
| enabled | bool | true |
Query param — true to enable, false to disable |
# Enable studio mode curl -X POST "http://localhost:8080/obs/studio/enable?enabled=true" # Disable studio mode curl -X POST "http://localhost:8080/obs/studio/enable?enabled=false"
Response
{ "studio_mode": true, "status": "ok" }
POST
/obs/studio/transition
Cut the preview scene to program output (Studio Mode only)
# Workflow: set up preview, then cut when ready curl -X POST "http://localhost:8080/obs/studio/enable?enabled=true" # ... configure your preview scene in OBS ... curl -X POST http://localhost:8080/obs/studio/transition
Response
{ "status": "transitioned" }
Scene Presets
Named broadcast states that combine scene switches with side effects (auto-mute, playlist activation)
Presets are the high-level control surface — you activate "brb" and it switches the scene, mutes the mic, and queues the BRB loop, all in one call. Configure them in
config.yaml to match your OBS scene names.
GET
/presets
List all registered scene presets and their OBS scene mappings
curl http://localhost:8080/presets
Response
[
{
"name": "live",
"scene_name": "Live",
"description": "Main live broadcast scene",
"playlist": null,
"hotkey": "/obs/scene/live",
"actions": []
},
{
"name": "brb",
"scene_name": "BRB",
"description": "Be Right Back screen",
"playlist": null,
"hotkey": "/obs/scene/brb",
"actions": [
{ "type": "set_mute", "params": { "source_name": "Mic", "muted": true } }
]
},
// ... standby, intermission, end_card ...
]
POST
/presets/{preset_name}/activate
Activate a preset — switches scene and runs all configured side effects
# Go live curl -X POST http://localhost:8080/presets/live/activate # BRB (also auto-mutes Mic per preset config) curl -X POST http://localhost:8080/presets/brb/activate # Standby / holding screen curl -X POST http://localhost:8080/presets/standby/activate # Intermission (activates linked playlist) curl -X POST http://localhost:8080/presets/intermission/activate # End of show slate curl -X POST http://localhost:8080/presets/end_card/activate
Response
{
"preset": "brb",
"actions": [
{ "action": "switch_scene", "scene": "BRB", "status": "ok" },
{ "action": "set_mute", "source": "Mic", "muted": true, "status": "ok" }
]
}
Playlists
M3U-based video playlist management with hot-swap between channels
How playlists work in obs-relay:
A playlist is a named list of video files (or URLs). When you activate a playlist, obs-relay loads the first item into your OBS Media Source input. When that video finishes (or you call
The key concept: obs-relay controls what file is loaded into OBS, not OBS's internal playback. You need a Media Source in OBS named
Multiple playlists are like TV channels — you define a "main" playlist, an "intermission" playlist, a "pre-show" playlist, etc. Switching between them is like Pluto.tv changing channels.
A playlist is a named list of video files (or URLs). When you activate a playlist, obs-relay loads the first item into your OBS Media Source input. When that video finishes (or you call
/next), obs-relay loads the next file into the same source — OBS plays it automatically.The key concept: obs-relay controls what file is loaded into OBS, not OBS's internal playback. You need a Media Source in OBS named
MediaSource (or whatever you set in config.yaml under
playlist.source_name). That source lives in your scene, and obs-relay keeps swapping files into it.Multiple playlists are like TV channels — you define a "main" playlist, an "intermission" playlist, a "pre-show" playlist, etc. Switching between them is like Pluto.tv changing channels.
Make sure you have a Media Source in OBS with the name matching
playlist.source_name in your config.yaml (default: MediaSource). The source should be set to a local file. obs-relay
will override that file path on each track change.
GET
/playlists
List all loaded playlists with their tracks and metadata
curl http://localhost:8080/playlists
Response
{
"main": {
"name": "main",
"item_count": 6,
"loop": true,
"shuffle": false,
"items": [
{ "path": "/media/holding.mp4", "title": "Pre-Show Holding", "duration": -1 },
{ "path": "/media/intro.mp4", "title": "Intro Bumper", "duration": 15 },
{ "path": "/media/block1.mp4", "title": "Content Block 1", "duration": 3600 }
]
},
"intermission": {
"name": "intermission",
"item_count": 3,
"loop": true,
"shuffle": false,
"items": [ /* ... */ ]
}
}
GET
/playlists/status
Current playback state — which playlist is active, which track is playing, position
curl http://localhost:8080/playlists/status
Response
{
"active_playlist": "main",
"position": 2,
"total_items": 6,
"loop": true,
"shuffle": false,
"current_item": {
"path": "/media/block1.mp4",
"title": "Content Block 1",
"duration": 3600
}
}
POST
/playlists/create
Create a playlist on-the-fly by sending a JSON list of file paths or URLs
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Unique name for this playlist |
| items | string[] | required | Array of local file paths or HTTP/RTMP URLs |
| loop | bool | optional | Loop when end is reached (default: true) |
| save | bool | optional | Save to disk as .m3u file (default: false) |
# Create a simple content block playlist from local files curl -X POST http://localhost:8080/playlists/create \ -H "Content-Type: application/json" \ -d '{ "name": "friday-night", "items": [ "/media/intro.mp4", "/media/episode-01.mp4", "/media/bumper.mp4", "/media/episode-02.mp4", "/media/outro.mp4" ], "loop": false, "save": true }' # Create a looping ambient playlist from HTTP streams curl -X POST http://localhost:8080/playlists/create \ -H "Content-Type: application/json" \ -d '{ "name": "ambient-loop", "items": [ "https://example.com/streams/ambient-a.mp4", "https://example.com/streams/ambient-b.mp4" ], "loop": true }'
Response
{
"name": "friday-night",
"item_count": 5,
"status": "created"
}
POST
/playlists/upload
Upload an existing .m3u or .m3u8 file from disk
You can also just drop .m3u files directly into the
playlists/ folder and restart the server — they're auto-loaded on startup. Upload is for adding playlists at runtime without a restart.curl -X POST http://localhost:8080/playlists/upload \ -F "file=@/path/to/my-show.m3u"
Example .m3u file format
#EXTM3U #EXTINF:15,Intro Bumper /media/intro.mp4 #EXTINF:3600,Main Content /media/main-show.mp4 #EXTINF:-1,Outro (unknown duration) /media/outro.mp4
Response
{ "name": "my-show", "item_count": 3, "status": "uploaded" }
POST
/playlists/{name}/activate
Switch to a playlist, loading its first (or specified) track into OBS
| Parameter | Type | Default | Description |
|---|---|---|---|
| position | int | 0 |
Query param — which track to start at (0-indexed) |
# Activate main playlist from track 0 curl -X POST http://localhost:8080/playlists/main/activate # Switch to intermission playlist (Pluto.tv channel change) curl -X POST http://localhost:8080/playlists/intermission/activate # Activate main playlist but start at track 3 (0-indexed) curl -X POST "http://localhost:8080/playlists/main/activate?position=3" # Switch to a pre-show holding playlist curl -X POST http://localhost:8080/playlists/pre-show/activate
Response
{
"playlist": "main",
"position": 0,
"track": "Pre-Show Holding Screen"
}
POST
/playlists/next
Advance to the next track in the active playlist. Loops back if at the end and loop=true.
curl -X POST http://localhost:8080/playlists/next
Response — track advanced
{ "track": "Content Block 2", "status": "advanced" }
Response — reached end of non-looping playlist
{ "track": null, "status": "end" }
POST
/playlists/prev
Go back to the previous track. Stops at position 0.
curl -X POST http://localhost:8080/playlists/prev
Response
{ "track": "Intro Bumper" }
POST
/playlists/seek/{position}
Jump directly to a track by index (0-based). Immediately loads that file into OBS.
# Jump to track 0 (first track / restart) curl -X POST http://localhost:8080/playlists/seek/0 # Jump to track 4 (fifth item) curl -X POST http://localhost:8080/playlists/seek/4
Response
{ "position": 4, "track": "Content Block 2" }
DELETE
/playlists/{name}
Remove a playlist from memory and delete its .m3u file from disk
curl -X DELETE http://localhost:8080/playlists/old-show
Response
{ "status": "deleted", "name": "old-show" }
Audio
Volume and mute control for OBS sources. Source names must match OBS exactly.
POST
/obs/source/{source_name}/volume
Set source volume in dB. Range: -100 (silence) to 26 (amplify).
| Field | Type | Range | Description |
|---|---|---|---|
| volume_db | float | -100 to 26 |
Volume in decibels. 0dB = unity gain (no change). -6dB = half perceived loudness. |
# Set Mic to unity gain (full volume) curl -X POST http://localhost:8080/obs/source/Mic/volume \ -H "Content-Type: application/json" \ -d '{ "volume_db": 0 }' # Reduce Desktop Audio by 6dB (roughly half) curl -X POST http://localhost:8080/obs/source/Desktop%20Audio/volume \ -H "Content-Type: application/json" \ -d '{ "volume_db": -6 }' # Near-silence (duck background music during speech) curl -X POST http://localhost:8080/obs/source/Background%20Music/volume \ -H "Content-Type: application/json" \ -d '{ "volume_db": -40 }'
Response
{ "source": "Mic", "volume_db": 0, "status": "ok" }
POST
/obs/source/{source_name}/mute
Mute or unmute an OBS audio source
# Mute the microphone (e.g. going to BRB) curl -X POST http://localhost:8080/obs/source/Mic/mute \ -H "Content-Type: application/json" \ -d '{ "muted": true }' # Unmute when returning from BRB curl -X POST http://localhost:8080/obs/source/Mic/mute \ -H "Content-Type: application/json" \ -d '{ "muted": false }'
Response
{ "source": "Mic", "muted": true, "status": "ok" }
Streaming
Start, stop, and monitor OBS stream output
GET
/obs/stream/status
Current stream state — live, timecode, bytes sent
curl http://localhost:8080/obs/stream/status
Response
{
"active": true,
"reconnecting": false,
"timecode": "00:42:15.000",
"bytes": 892401824
}
POST
/obs/stream/start
Begin streaming to your configured RTMP destination
curl -X POST http://localhost:8080/obs/stream/start
Response
{ "status": "streaming_started" }
POST
/obs/stream/stop
Stop the active stream
curl -X POST http://localhost:8080/obs/stream/stop
Response
{ "status": "streaming_stopped" }
WebSocket Relay
Persistent bidirectional connection — send commands, receive real-time events from OBS
WebSocket vs REST: Use REST for one-shot commands from scripts and backends. Use WebSocket when you're building a UI that needs to stay in sync — the WS connection receives push events whenever state changes (scene
switched, track changed, stream started), so your UI can update without polling.
WS
/ws
Persistent WebSocket connection endpoint
# Connect with wscat (npm install -g wscat) wscat -c ws://localhost:8080/ws # JavaScript browser / Node.js const ws = new WebSocket('ws://localhost:8080/ws'); ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log('Event:', msg.event, msg.data); }; ws.onopen = () => { // Send a command ws.send(JSON.stringify({ cmd: 'activate_preset', params: { name: 'brb' } })); };
WS
Commands — send JSON to control OBS
Format:
{"cmd": "command_name", "params": {...}}switch_scene
{ "cmd": "switch_scene", "params": { "scene_name": "BRB" } }
activate_preset
{ "cmd": "activate_preset", "params": { "name": "brb" } }
playlist_activate
{ "cmd": "playlist_activate", "params": { "name": "intermission" } }
playlist_next
{ "cmd": "playlist_next" }
stream_start / stream_stop
{ "cmd": "stream_start" }
{ "cmd": "stream_stop" }
get_status — poll current state
{ "cmd": "get_status" }
// Response:
{
"obs_connected": true,
"playlist": {
"active_playlist": "main",
"position": 2,
"current_item": { "title": "Content Block 1", ... }
}
}
WS
Events — pushed to all connected clients
Fired automatically whenever state changes from any source (REST, OSC, or another WS client)
scene_switched
{ "event": "scene_switched", "data": { "scene": "BRB", "status": "ok" } }
preset_activated
{ "event": "preset_activated", "data": { "preset": "brb", "actions": [...] } }
playlist_activated
{ "event": "playlist_activated", "data": { "playlist": "main", "track": "Intro Bumper" } }
track_changed
{ "event": "track_changed", "data": { "track": "Content Block 2", "status": "advanced" } }
stream_started / stream_stopped
{ "event": "stream_started", "data": { "status": "streaming_started" } }
{ "event": "stream_stopped", "data": { "status": "streaming_stopped" } }
OSC / TouchOSC
UDP OSC bridge — control obs-relay from TouchOSC, Lemur, or any OSC-capable device
GET
/osc/status
OSC bridge connection state and port info
curl http://localhost:8080/osc/status
Response
{
"enabled": true,
"running": true,
"listen_port": 9000,
"reply_port": 9001
}
OSC
Complete OSC Address Map
Send to port 9000 · Feedback received on port 9001
Scene Presets — send any value (e.g. 1.0)
| Address | Value | Action | |
|---|---|---|---|
| /obs/scene/live | 1.0 | send | Switch to Live preset |
| /obs/scene/brb | 1.0 | send | Switch to BRB preset |
| /obs/scene/standby | 1.0 | send | Switch to Standby |
| /obs/scene/intermission | 1.0 | send | Switch to Intermission |
| /obs/scene/end_card | 1.0 | send | Switch to End Card |
Playlist Control
| Address | Value | Action | |
|---|---|---|---|
| /obs/playlist/next | 1.0 | send | Next track |
| /obs/playlist/prev | 1.0 | send | Previous track |
| /obs/playlist/activate/main | 1.0 | send | Load "main" playlist |
| /obs/playlist/activate/intermission | 1.0 | send | Load intermission playlist |
| /obs/playlist/seek | int | send | Jump to track index |
Stream
| Address | Value | Action | |
|---|---|---|---|
| /obs/stream/start | 1.0 | send | Start streaming |
| /obs/stream/stop | 1.0 | send | Stop streaming |
Audio
| Address | Value | Action | |
|---|---|---|---|
| /obs/volume/Mic | 0.0–1.0 | send | Mic volume (maps to -60–0dB) |
| /obs/volume/Desktop | 0.0–1.0 | send | Desktop audio volume |
| /obs/mute/Mic | 0 or 1 | send | Mute (1) / unmute (0) Mic |
State Feedback — received from obs-relay on port 9001
| Address | Type | Content | |
|---|---|---|---|
| /obs/state/scene | string | recv | Current scene name |
| /obs/state/stream | int | recv | 1 = streaming, 0 = not |
| /obs/state/playlist | string | recv | Active playlist name |
| /obs/state/track | string | recv | Current track title |
| /obs/state/query | 1.0 | send | Request full state broadcast |