🟢

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 /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