Docs / Nebula Sound System manual / 9. The Control API
9. The Control API
The Control API is a small local door into a running Nebula: switch it on, and any program on your own computer can ask Nebula to do the things you could do by clicking in the window — add a track, write notes, put an effect on, render to a file — as real, undoable edits. It is the foundation both hardware controllers (chapter 8, a thinner one-way version of it) and AI assistants (chapter 10, the MCP server) are built on.
The full, formal contract — every method, every parameter, every reply shape — lives in the
API repository as an OpenRPC document:
schemas/control-api/openrpc.json. This chapter is the plain-English tour; that file is the
source of truth if the two ever disagree.
Turning it on
Options > Settings > Local API, switch it on. Nebula:
- generates a random token the first time you enable it, and stores it in your settings
file (
api.token, insidesettings.jsonin Nebula's data folder — see chapter 1 for where that is) — you never have to type it, but a script needs to read it once to connect; - listens on
127.0.0.1only — never your network — on TCP port 8642 by default (changeable in the same settings page).
Speaking to it directly
The wire protocol is JSON-RPC 2.0, one JSON object per line, over that TCP socket. Every
request carries your token; a wrong or missing one is refused (-32001) before anything runs.
Replies and events (things Nebula tells you without being asked, like "the project changed")
share the same connection — match a reply to your request by its id; a line with a method
field instead of a result is an event.
You will almost never write this by hand, though — eight ready-made client libraries exist, one
per language, each one file (or a small folder), each proven against a real running Nebula:
Python, TypeScript/JavaScript, C#, Go, Rust, Java, Ruby and Lua. Grab whichever matches your
project from clients/<language>/ in the API repository.
A two-minute Python quick-start
import sys
sys.path.insert(0, "clients/python") # wherever you put nebula_client.py
from nebula_client import NebulaClient
with NebulaClient(port=8642, token="paste-your-token-here") as n:
print(n.ping()) # {'version': '0.2.0-...', 'api': 1, 'contract': '1.3'}
track = n.add_track()["id"]
n.set_track_instrument(track, "Modular Lead")
n.add_pattern(track) # a ready-made 4-bar starter pattern
n.set_tempo(128)
n.play()
render = n.render_to_wav(timeout=60) # waits for the project to finish rendering
print("Wrote", render["path"])
n.undo() # every one of the edits above can be undone
And the same thing in Lua
local nebula = require("nebula_client") -- needs LuaJIT -- see clients/lua/README.md
nebula.open({port = 8642, token = "paste-your-token-here"}, function(n)
print(n:ping().version)
local track = n:add_track().id
n:set_track_instrument(track, "Modular Lead")
n:add_pattern(track)
n:set_tempo(128)
n:play()
local render = n:render_to_wav(60)
print("Wrote " .. render.path)
n:undo()
end)
What you can do
- Read:
project.get,tracks.list,clips.list/clips.notes,devices.catalogue/devices.knobs/devices.list,instruments.list,transport.position,history.info. - Compose: add/rename/remove tracks, add a pattern or an empty/audio clip, add/edit/remove notes, quantize a clip, move/resize/split/duplicate a clip.
- Mix: add/remove an effect (optionally from a factory preset), bypass it, reorder it, turn any of its knobs.
- Transport & history: play, stop, set the tempo, undo, redo.
- Render:
project.renderToWavstarts a render in the background; anebula/render.finishedevent (carrying the file's path and length) tells you when it is done — the client libraries'render_to_wav()helper waits for that for you.
A request naming something that does not exist — an unknown track, clip, note or effect id, an
instrument or effect name Nebula does not have — is refused with a clear error (-32602) that
names the method to call to look the right value up, rather than silently doing nothing.
One client at a time
The door serves one connected script at a time. If a second one connects while the first is
still there, it gets a single "the door is busy" reply (-32004) and is disconnected — the first
script's own traffic is never affected. Close one before opening another.
Everything is real, everything is undoable
There is no separate "script mode" — a request from a script runs through the exact same
undo-tracked edit path as a click in the window. Ctrl+Z in the window undoes a script's last
edit exactly like it would undo your own; a script calling history.undo undoes the last edit
whoever made it, script or person.
Updated 2026-10-31