Skip to content

Ableton Live REST API

Producer Pal includes a REST API for building custom scripts, automation, and integrations with Ableton Live using plain HTTP requests — no MCP SDK needed. It also works as an alternative interface for coding agents: download this page as Markdown (button at the top) and give it to your agent for a complete reference.

The REST API runs on the same server as the MCP endpoint (default port 3350) and is available whenever the Producer Pal Max for Live device is running.

This is for developers

Most users don't need the REST API. The normal way to use Producer Pal is through an AI chat client like Claude Desktop — see the Installation guide to get started.

Endpoints

List Tools

GET http://localhost:3350/api/tools

Returns all enabled tools with their JSON Schema input definitions:

json
{
  "tools": [
    {
      "name": "ppal-read-live-set",
      "title": "Read Live Set",
      "description": "Read an overview of the Live Set...",
      "annotations": { "readOnlyHint": true, "destructiveHint": false },
      "inputSchema": { "type": "object", "properties": { ... } }
    }
  ]
}

Call a Tool

POST http://localhost:3350/api/tools/{toolName}
Content-Type: application/json

{ "trackIndex": 0, "include": ["session-clips"] }

Returns (the default json format — see Response format):

json
{ "result": { "...": "..." }, "isError": false }
  • 200 with isError: false — tool ran successfully
  • 200 with isError: true — tool ran but reported an error (e.g. invalid path, execution error)
  • 404 — unknown or disabled tool
  • 400 — invalid input (includes validation details)
  • 504 — the tool didn't finish before the timeout (see Per-request timeout)
  • 500 — internal server error

504 and 500 use a different body shape

The error responses do not carry result / isError. A 504 returns { "error": "...", "errorCode": "timeout" } — check errorCode to distinguish a timeout from other failures — and a 500 returns { "error": "..." }. Client code that reads body.result unconditionally will break on exactly the case the timeoutMs parameter below invites you to hit.

Warnings from the Live API surface as a separate warnings string array (or inline in the result text under ?format=compact). The ppal-update-* tools use this when updating multiple objects — if any individual operation fails or is inapplicable (e.g. setting quantize on an audio clip), it emits a warning and continues with the rest.

Response format: ?format=json (default)

The REST API defaults to json: result is the parsed value (object, array, number, string) and warnings are a separate string[]. This is the right default for HTTP integrations — no JSON.parse or jq | fromjson gymnastics. The device-level JSON Output setting (Setup tab) does not affect the REST API.

The compact JS-literal format (unquoted keys, no whitespace) is optimized for LLM token efficiency and is the same format MCP clients receive. It is opt-in for REST via ?format=compact, where result is a string with warnings inline:

bash
# JSON (default) — result is the parsed value; warnings are a separate string array
curl -X POST http://localhost:3350/api/tools/ppal-read-live-set \
  -H 'Content-Type: application/json' -d '{}'
# → {"result":{"tempo":120,"timeSignature":"4/4",...},"isError":false}

# Compact JS-literal — result is a string, warnings are inline
curl -X POST 'http://localhost:3350/api/tools/ppal-read-live-set?format=compact' \
  -H 'Content-Type: application/json' -d '{}'
# → {"result":"{tempo:120,timeSignature:\"4/4\",...}","isError":false}

With the default json format (or explicit ?format=json):

  • result is the parsed value (object, array, number, string, etc.) — not a JSON-encoded string. Access fields directly: body.result.tempo.
  • warnings is a string[] (with the WARNING: prefix stripped), present only when the tool emitted any. In compact mode, warnings remain inline in result for backwards compatibility.
  • appended is a string[] of extra Markdown text blocks the server attaches after the result. Currently only ppal-connect uses it, to deliver — in order — the Producer Pal skills (notation instructions), this Live Set's project context, your ~/.producer-pal/context.md global context, your memory index, and a final next-step block. The context blocks are self-labeling (Project context (this Live Set):, Global context (all projects):, Memory index — …) and only appear when you've configured them; the skills and the next-step block are always present, so appended is never empty on ppal-connect. The next-step block names any empty context layers and tells the AI what to do next, so don't assume the last element is context. In compact mode these blocks are joined into the result string instead.
  • On error (isError: true), result is still a plain error string regardless of format — error messages are not JSON.

Pass ?format=compact to opt into the compact JS-literal format, or ?format=json to be explicit about the default. Other values return 400. The REST format is independent of the device-level setting and never affects MCP clients.

Per-request timeout: ?timeoutMs=N

Override the configured tool-call timeout for a single request. Useful for long-running operations (e.g. bulk clip generation) that need more headroom than the global timeout, or for short polling calls that should fail fast.

bash
curl -X POST 'http://localhost:3350/api/tools/ppal-create-clip?timeoutMs=10000' \
  -H 'Content-Type: application/json' \
  -d '{"path": "t0/s0", "length": "16bar", "notes": "..."}'

timeoutMs must be a positive integer up to 55000 (55 seconds). Other values return 400. The cap stays under 60 seconds because that is where most MCP clients give up — past it you lose the partial results and warnings Producer Pal returns on a timeout. Combinable with ?format=:

POST /api/tools/{name}?format=json&timeoutMs=10000

Per-request settings

Three headers let one client run its own profile. They work on both REST endpoints and on the MCP endpoint, so a script, an agent, and the Chat UI can each use a different notation at the same time without a POST /config changing everyone else's:

HeaderValueOverrides
x-producer-pal-disabled-toolscomma-separated tool namesthe toolset
x-producer-pal-small-model-modetrue / falsesmall model mode
x-producer-pal-notationbarbeat, midi-json, starkthe notation

Absent or unrecognized values fall back to the device's global setting, so clients that send nothing are unaffected. Nothing is remembered between requests — send the headers on every request, GET /api/tools included, so the schemas you read match what you send.

Notation

x-producer-pal-notation picks the MIDI notation for one request. It decides the note syntax in the tool and argument descriptions GET /api/tools serves, the syntax the Skills teach, and — unlike the other two — how notes in your arguments are parsed and how notes in the response are formatted.

bash
# Read a clip's notes as a JSON array, whatever the device is set to
curl -X POST http://localhost:3350/api/tools/ppal-read-clip \
  -H 'Content-Type: application/json' \
  -H 'x-producer-pal-notation: midi-json' \
  -d '{"id": "123", "include": ["notes"]}'

This is the header to reach for in a coding agent: midi-json gives you notes you can build and parse programmatically, without forcing the user's Chat UI off barbeat mid-session.

Toolset

x-producer-pal-disabled-tools withholds tools from a single request. A withheld tool disappears from GET /api/tools and 404s from POST /api/tools/{name}.

The reason to bother is ppal-connect: withholding a tool also drops the part of the Skills it returns that teaches that tool, so a client that only needs a few tools stops paying for the rest in every session. See Choosing a Toolset.

bash
# A read-only session: no writers, and no note-writing instructions either
curl -X POST http://localhost:3350/api/tools/ppal-connect \
  -H 'Content-Type: application/json' \
  -H 'x-producer-pal-disabled-tools: ppal-create-clip,ppal-update-clip,ppal-delete' \
  -d '{}'

Unrecognized names are ignored. ppal-connect itself can be withheld — nothing is reserved here, unlike the npx producer-pal flags.

Small model mode

x-producer-pal-small-model-mode: true shrinks the tool schemas GET /api/tools serves and switches the Skills to the shorter variant. It's aimed at local and lightweight models; see Small Model Mode.

Output format and timeout are REST-only

?format= and ?timeoutMs= above have no MCP equivalent, on purpose. Query params aren't something MCP clients send, /mcp is built to return the one MCP-shaped response, and the timeout exists for slow machines — a fact about the device, not about one call. Set it on the device's Setup tab.

Quick Start with curl

bash
# Read the Live Set overview
curl -X POST http://localhost:3350/api/tools/ppal-read-live-set \
  -H 'Content-Type: application/json' -d '{}'

# Read track 0 with all clips
curl -X POST http://localhost:3350/api/tools/ppal-read-track \
  -H 'Content-Type: application/json' \
  -d '{"trackIndex": 0, "include": ["session-clips", "arrangement-clips"]}'

# List available tools
curl http://localhost:3350/api/tools

Sample Scripts

Zero-dependency client examples — they use only built-in HTTP libraries. Copy and modify them for your own integrations.

Node.js

The Node.js client doubles as the Producer Pal Agent Skill script — see The bundled script for the full source and CLI reference. It works with Claude Code, Codex CLI, Gemini CLI, and any other agent runtime that reads the SKILL.md convention.

Looping calls with a sleep in between? Node's fetch will stall

Node's built-in fetch is undici, and the copy Node 26 vendors (8.9.0) leaves a request unsent on an idle keep-alive connection until your script's event loop wakes for something else. If that something else is your own sleep, the gap plus the stall snaps to 500ms — or 3 seconds for gaps under 3 seconds. A loop sleeping 1s between calls spends ~3s per call instead of ~25ms.

It only affects scripts that sleep between requests; one-shot calls and back-to-back loops are fine. Either use node:http instead of fetch, or install a dispatcher from a version without the bug (undici fixed it in 8.10.0; 7.x predates it — but 8.9.0 itself will not help):

js
import { Agent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new Agent());

This is a Node client-side issue, not something Producer Pal can fix from the device, and it will inflate any latency you measure this way. It goes away on its own once Node ships undici 8.10.0 or newer.

Python

Works with Python 3.6+ (no dependencies).

py
#!/usr/bin/env python3

"""Producer Pal REST API client (Python, no dependencies).

Usage:
  python ppal.py --list-tools [options]
  python ppal.py <tool-name> [json-args] [options]

Options:
  --url <baseUrl>          override Producer Pal URL (default http://localhost:3350)
  --timeout-ms <ms>        per-request timeout (1-60000)
  --notation <name>        barbeat | midi-json | stark, for this request only
  --disable-tools <names>  withhold tools from this request (comma-separated)
  --small-model-mode       shrink tool schemas and Skills for this request

Examples:
  python ppal.py --list-tools --notation midi-json
  python ppal.py ppal-read-live-set
  python ppal.py ppal-read-track '{"trackIndex": 0}'
  python ppal.py --list-tools | jq -r '.tools[].name'
  python ppal.py ppal-read-live-set | jq .result.tempo
  python ppal.py ppal-connect --disable-tools ppal-library,ppal-create-device
"""

import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request

# The three per-request headers. Unlike POST /config these change nothing on the
# device: each applies to the one request that carries it, so it can't move the
# chat UI or another client off its own notation or toolset. Absent ⇒ that client
# keeps the device's global setting.
DISABLED_TOOLS_HEADER = "x-producer-pal-disabled-tools"
NOTATION_HEADER = "x-producer-pal-notation"
SMALL_MODEL_MODE_HEADER = "x-producer-pal-small-model-mode"


def profile_headers(*, disabled_tools=None, notation=None, small_model_mode=None):
    """Headers for this call's profile, omitting whichever values are absent.

    `disabled_tools` is a comma-separated string or a list of tool names;
    `notation` is "barbeat", "midi-json", or "stark"; `small_model_mode` is a
    bool. Nothing is remembered between requests, so pass the same values on
    every call in a session — the list_tools call included, so the schemas you
    read match the notation you'll write.
    """
    names = disabled_tools if isinstance(disabled_tools, str) else ",".join(disabled_tools or [])
    headers = {}
    if names.strip():
        headers[DISABLED_TOOLS_HEADER] = names
    if notation:
        headers[NOTATION_HEADER] = notation
    if small_model_mode is not None:
        headers[SMALL_MODEL_MODE_HEADER] = "true" if small_model_mode else "false"
    return headers


def list_tools(base_url, *, disabled_tools=None, notation=None, small_model_mode=None):
    """GET /api/tools — returns the full envelope `{"tools": [...]}` as a dict.

    The tool list endpoint always returns JSON; it has no `?format` toggle. The
    profile arguments shape the catalog: withheld tools are omitted, and the
    descriptions and schemas resolve against this request's notation and
    small-model mode.
    """
    req = urllib.request.Request(
        f"{base_url}/api/tools",
        headers=profile_headers(
            disabled_tools=disabled_tools,
            notation=notation,
            small_model_mode=small_model_mode,
        ),
    )
    with urllib.request.urlopen(req) as res:
        return json.loads(res.read())


def call_tool(
    base_url,
    name,
    args,
    *,
    timeout_ms=None,
    disabled_tools=None,
    notation=None,
    small_model_mode=None,
):
    """Call a Producer Pal tool by name with the given args.

    The REST API defaults to `format=json`, so `result` is a parsed value
    (dict/list/etc.) and warnings are surfaced as a separate `warnings` list.

    The profile arguments apply to this request: a withheld tool 404s,
    `ppal-connect` returns a Skills blob matching this request's notation and
    toolset, and `notation` also decides how notes in the arguments are parsed
    and how notes in the result are formatted.
    """
    params = {}
    if timeout_ms is not None:
        params["timeoutMs"] = str(timeout_ms)

    query = f"?{urllib.parse.urlencode(params)}" if params else ""
    url = f"{base_url}/api/tools/{name}{query}"
    data = json.dumps(args).encode()
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Content-Type": "application/json",
            **profile_headers(
                disabled_tools=disabled_tools,
                notation=notation,
                small_model_mode=small_model_mode,
            ),
        },
        method="POST",
    )
    with urllib.request.urlopen(req) as res:
        return json.loads(res.read())


def main():
    parser = argparse.ArgumentParser(
        description="Producer Pal REST API client",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  python ppal.py --list-tools --notation midi-json\n"
            "  python ppal.py ppal-read-live-set\n"
            '  python ppal.py ppal-read-track \'{"trackIndex": 0}\'\n'
            "\n"
            "--notation, --disable-tools, and --small-model-mode apply to the ONE\n"
            "request that carries them. Pass them every time, --list-tools included."
        ),
    )
    parser.add_argument("tool", nargs="?", help="Tool name to call")
    parser.add_argument(
        "args",
        nargs="?",
        default="{}",
        help="Tool arguments as a JSON object (default: {})",
    )
    parser.add_argument("--url", default="http://localhost:3350")
    parser.add_argument("--timeout-ms", type=int, default=None, dest="timeout_ms")
    parser.add_argument("--list-tools", action="store_true", dest="list_tools")
    parser.add_argument(
        "--disable-tools",
        default=None,
        dest="disabled_tools",
        help="Comma-separated tool names to withhold from this request",
    )
    parser.add_argument(
        "--notation",
        default=None,
        choices=["barbeat", "midi-json", "stark"],
        help="MIDI notation for this request",
    )
    parser.add_argument(
        "--small-model-mode",
        action="store_true",
        default=None,
        dest="small_model_mode",
        help="Shrink tool schemas and Skills for this request",
    )
    parsed = parser.parse_args()

    if parsed.list_tools:
        listing = list_tools(
            parsed.url,
            disabled_tools=parsed.disabled_tools,
            notation=parsed.notation,
            small_model_mode=parsed.small_model_mode,
        )
        print(json.dumps(listing, indent=2))
        return

    if not parsed.tool:
        parser.error(
            "Missing tool name. Use --list-tools to discover tools, "
            "or pass a tool name as the first argument."
        )

    try:
        tool_args = json.loads(parsed.args)
    except json.JSONDecodeError as e:
        parser.error(f"Invalid JSON for tool args: {e}")

    response = call_tool(
        parsed.url,
        parsed.tool,
        tool_args,
        timeout_ms=parsed.timeout_ms,
        disabled_tools=parsed.disabled_tools,
        notation=parsed.notation,
        small_model_mode=parsed.small_model_mode,
    )
    if response.get("isError"):
        print(f"API error: {response['result']}", file=sys.stderr)
        sys.exit(1)
    print(json.dumps(response, indent=2))


if __name__ == "__main__":
    try:
        main()
    except urllib.error.URLError as e:
        if "Connection refused" in str(e.reason):
            print(
                "Could not connect to Producer Pal."
                " Is Ableton Live running with the Producer Pal device?",
                file=sys.stderr,
            )
        else:
            raise
        sys.exit(1)

Tool Reference

Use the list tools endpoint to discover all available tools and their input schemas at runtime. You can also browse the full tool documentation in the Tool Reference.

Live API

The ppal-live-api tool provides direct access to the Ableton Live Object Model for scripting and debugging.

It is opt-in: enable Direct Live API on the Setup tab of the Producer Pal Max for Live device, or programmatically with a POST /config request (from curl or a same-origin script — cross-origin browser writes to /config are rejected):

bash
curl -X POST http://localhost:3350/config \
  -H 'Content-Type: application/json' \
  -d '{"liveApiEnabled": true}'

The setting is global to the device (it also affects the Chat UI and any connected MCP clients). This is an advanced escape hatch — the higher-level tools are tuned for reliable results, so reach for the raw Live API only for custom integrations, scripting, or debugging when the standard tools aren't enough.

Request structure

The path parameter sets the initial Live Object Model object to operate on (e.g., "live_set", "live_set tracks 0", "live_set tracks 0 clip_slots 1 clip"). The operations array is then executed sequentially on that object. Use goto to navigate to a different object mid-sequence.

Available operation types:

TypeProperties usedDescription
getpropertyRead a property's raw value — a _list property returns the full array. Returns the number 1, not an array, when the object doesn't exist
setproperty, valueWrite a property value. Always returns 1, even when the write is rejected — read the property back to confirm it landed
set_propertyproperty, valueThe same write as set, but returns the value you sent
callmethod, args (optional)Call a method on the Live object
gotovalue (path)Navigate to a different object
infoGet object info
getcountproperty (child type)Count the object's children in a collection. 0 when the object doesn't exist
getstringpropertyRead a property as a string. Returns the number 1, not a string, when the object doesn't exist
getPropertypropertyRead a property, unwrapped to a scalar — truncates a _list property to its first element; use get for the full array
getChildIdsproperty (child type)Get child object IDs
existsCheck if the object exists. Producer Pal's judgment, not Live's: Live's own valid field reads 1 even for a bad path, so this checks the id
getColorRead object color
setColorvalue (hex string)Write object color
get_propertypropertyRead a JavaScript field on the LiveAPI object itself (path, id, type, mode, valid, children, …), not a Live property
call_methodmethod, args (optional)Call a JavaScript method on the LiveAPI object itself (getProperty, getChildIds, child, …), not a Live method
set_pathvalue (path)Assign the LiveAPI object's path, retargeting it. "" clears it
set_modevalue (0 or 1)Assign the LiveAPI object's mode: 0 follows the path, 1 follows the object

The last group operates on the JavaScript wrapper, not the Live object it points at. Despite the names, get/get_property and call/call_method are not aliases — call get_current_beats_song_time works, while call_method get_current_beats_song_time fails because that method lives on the Live object, not the wrapper. Only set and set_property perform the same write, and even they report different results.

When the object doesn't exist

A bad path, a bad index, a bad id and a path cleared to "" all behave the same way, and none of them raise an error. Verified against Live 12.4.3:

  • get, set, call and getstring return the number 1
  • getcount returns 0
  • info returns "No object"

Read a bare 1 as "no object, no answer". It is not a success flag — set returns 1 on a perfectly valid object too, whether or not the write landed. A read-only property, a wrong-typed value, an unknown property and an out-of-range value all return 1 and change nothing. Read the property back if you need to know whether a write took.

Normalizing that away is most of what the Producer Pal operations add over the raw Live ones: getProperty gives undefined, getChildIds gives [], getColor gives null, and exists gives false. Prefer exists over reading Live's own valid field, which reads 1 in all four cases — it describes the wrapper object, not the target it points at.

You don't need to call set_path "" yourself for cleanup. Live arms a path listener on every collection along a path-based object's path and never takes them down, so every LiveAPI object a request creates has its path cleared once the request ends, whether or not it succeeded.

Examples

bash
# Get the tempo
curl -X POST http://localhost:3350/api/tools/ppal-live-api \
  -H 'Content-Type: application/json' \
  -d '{
    "path": "live_set",
    "operations": [{"type": "getProperty", "property": "tempo"}]
  }'

# Set the tempo to 140 BPM
curl -X POST http://localhost:3350/api/tools/ppal-live-api \
  -H 'Content-Type: application/json' \
  -d '{
    "path": "live_set",
    "operations": [{"type": "set_property", "property": "tempo", "value": 140}]
  }'

# Fire scene 0
curl -X POST http://localhost:3350/api/tools/ppal-live-api \
  -H 'Content-Type: application/json' \
  -d '{
    "path": "live_set",
    "operations": [{"type": "call", "method": "fire_scene_at_index", "args": [0]}]
  }'

# Chain multiple operations on one object
curl -X POST http://localhost:3350/api/tools/ppal-live-api \
  -H 'Content-Type: application/json' \
  -d '{
    "path": "live_set tracks 0",
    "operations": [
      {"type": "get", "property": "name"},
      {"type": "get", "property": "color_index"},
      {"type": "get", "property": "has_midi_input"}
    ]
  }'

INFO

When the Direct Live API toggle is off on the device Setup tab, requests to ppal-live-api return 404.

Tips

  • The inputSchema in the tool list response is standard JSON Schema, so you can use it for client-side validation or code generation.
  • The REST API shares the same tool configuration as MCP — tools enabled or disabled on the device apply to both interfaces.
  • The REST API has no authentication (same as the MCP endpoint). It is designed for use on localhost or trusted networks only.
  • Browser pages can only call the REST API from localhost origins by default (ENABLE_REMOTE_CORS widens this to any origin). curl and other non-browser clients ignore CORS entirely and are unaffected either way.

Released under the GPL-3.0 License.