Quickstart
Everything below works against a running launcher, v5.7.1 or later. The API server is always on: it binds api.sock at launcher startup, whether you started the GUI or headless mode.
Socket path (<launcher_dir>/api.sock):
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/EkaiiLauncher/api.sock |
| Linux | ~/.local/share/EkaiiLauncher/api.sock |
| Portable | <portable_dir>/EkaiiLauncher/api.sock |
| Windows | Named pipe \\.\pipe\pandora-launcher-api (server works; the bundled --api client is Unix-only and exits with code 2 on Windows, but any client that speaks the protocol over the pipe works) |
1. The bundled CLI (--api)
The launcher binary doubles as a client: it connects to the running launcher's socket, sends one command, prints the raw JSON response line and exits. It never takes the lockfile and never opens a window.
ekaii-launcher --api launcher.version
ekaii-launcher --api launcher.status
ekaii-launcher --api instances.list | jq '.data[].name'
ekaii-launcher --api instances.start --api-params '{"name": "main"}'
Exit codes: 0 = response with "ok": true (or clean end of an event stream), 1 = response with "ok": false, 2 = transport error (launcher not running, bad --api-params JSON, connection dropped).
2. Raw socket, NDJSON
The protocol is one JSON object per line, \n terminated. Requests carry id, cmd and optional params; the response echoes your id:
SOCK="$HOME/Library/Application Support/EkaiiLauncher/api.sock" # macOS
# SOCK="$HOME/.local/share/EkaiiLauncher/api.sock" # Linux
echo '{"id":1,"cmd":"launcher.version"}' | nc -U "$SOCK"
# Pipelining works; requests on one connection are processed sequentially
printf '%s\n%s\n' \
'{"id":1,"cmd":"instances.list"}' \
'{"id":2,"cmd":"accounts.list"}' | nc -U "$SOCK"
Success: {"id":1,"ok":true,"data":{...}}. Failure: {"id":1,"ok":false,"error":{"code":"not_found","message":"...","retryable":false}}. Branch on error.code, never on message text.
3. Python
A minimal client with id correlation (event lines, which carry an event key instead of id/ok, are treated as out-of-band):
import json, os, socket
SOCK = os.path.expanduser("~/Library/Application Support/EkaiiLauncher/api.sock") # macOS
# Linux: os.path.expanduser("~/.local/share/EkaiiLauncher/api.sock")
class Pandapi:
def __init__(self, path=SOCK):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path)
self.buf = b""
self.next_id = 0
def _readline(self):
while b"\n" not in self.buf:
chunk = self.sock.recv(65536)
if not chunk:
raise ConnectionError("launcher closed the connection")
self.buf += chunk
line, self.buf = self.buf.split(b"\n", 1)
return line
def call(self, cmd, params=None):
self.next_id += 1
req = {"id": self.next_id, "cmd": cmd, "params": params or {}}
self.sock.sendall(json.dumps(req).encode() + b"\n")
while True:
msg = json.loads(self._readline())
if "event" in msg:
continue # out-of-band event line (only after events.subscribe)
if msg.get("id") == self.next_id:
if not msg["ok"]:
e = msg["error"]
raise RuntimeError(f"{e['code']}: {e['message']} (retryable={e['retryable']})")
return msg["data"]
api = Pandapi()
print(api.call("launcher.version"))
# Idempotent provisioning, then a launch that blocks until the game runs
api.call("instances.ensure", {"name": "api-demo", "version": "26.2", "loader": "fabric",
"memory": {"enabled": True, "min": 512, "max": 4096}})
op = api.call("instances.start", {"name": "api-demo"})["op"]
print(api.call("ops.wait", {"op": op, "timeout_secs": 600}))
4. Discover the surface: api.describe
The API is self-describing. One call returns every command (grouped, with one-line descriptions), every event type, and a machine-readable security block (scopes, the read-only set, the local-only set, denied fields, env vars, error codes):
ekaii-launcher --api api.describe | jq '.data.commands | keys'
ekaii-launcher --api api.describe | jq '.data.events'
ekaii-launcher --api api.describe | jq '.data.security'
This is the intended bootstrap for SDK generation and for LLM agents (see Automation-and-AI). Over the network it needs only the read scope.
5. A first real workflow
# Create an instance (asynchronous; it appears in instances.list within a second or two)
ekaii-launcher --api instances.create --api-params '{"name":"api-demo","version":"26.2","loader":"fabric"}'
# Install a mod from Modrinth (returns an op)
ekaii-launcher --api content.install --api-params \
'{"name":"api-demo","files":[{"modrinth":{"project_id":"AANobbMI"}}]}'
# Block until that op finishes
ekaii-launcher --api ops.wait --api-params '{"op": 1}'
# Launch and block until the game process is running (wait: "running" | "exited")
ekaii-launcher --api instances.start --api-params '{"name":"api-demo","wait":"running","timeout_secs":600}'
# Plain-language status, then kill
ekaii-launcher --api status.summary
ekaii-launcher --api instances.kill --api-params '{"name":"api-demo"}'
Long-running commands return {"op": N} immediately; poll ops.status, block with ops.wait, or subscribe to op.* events. Fire-and-forget mutations return {"requested": true}, meaning enqueued, not applied: read back to verify. See Concepts.
6. Events
events.subscribe switches the connection (or the CLI) to streaming mode:
ekaii-launcher --api events.subscribe
{"id":0,"ok":true,"data":{"subscribed":true,"current_seq":41,"session_id":"18f3c0a1b2"}}
{"seq":42,"event":"instance.modified","data":{"id":"0:1","name":"api-demo","status":"launching",...}}
{"seq":43,"event":"instance.modified","data":{"id":"0:1","name":"api-demo","status":"running",...}}
Every event carries a monotonic seq; reconnect with {"since_seq": <last seen>} to replay missed events from the 4096-event ring, and filter server-side with types / instance. See Concepts.
7. Headless mode
--headless runs the backend and the control API without opening any window, turning the launcher into a scriptable daemon:
ekaii-launcher --headless &
# There is no game-output window headless, so opt into console capture
# (applies to instances launched after the call) to get game.output and game.* events:
ekaii-launcher --api game.capture --api-params '{"enabled": true}'
ekaii-launcher --api instances.start --api-params '{"name":"api-demo","wait":"running"}'
# Stop the daemon (local-socket-only command)
ekaii-launcher --api launcher.quit
Headless mode still takes the single-instance lock and binds the socket exactly as a normal launch would.
Next
- Concepts for the model behind all of this.
- Command-Reference for the full grouped surface.
- Network-Mode-and-Security to reach the API over TCP/WebSocket with scoped tokens.
- Full parameter schemas: docs/API.md.