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, inside settings.json in 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.1 only — 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.renderToWav starts a render in the background; a nebula/render.finished event (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

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.