Skip to content

API Guide

OverMox ships a full local API. Everything you can do in the app, you can drive programmatically over HTTP and WebSocket against the OverMox app running on your own machine at http://localhost:5000. There are three lenses on it: this guide, the REST reference, and the live event stream.

You can also view the raw llms.txt, the same machine-readable API guide OverMox serves, and feed it to your own AI assistant.

REST + WebSocket API for OverMox, a realtime 3D creative environment, and its companion visual node graph editor (OverMox Controller).

Website: https://overmox.com Documentation: https://overmox.com/docs Base URL: http://localhost:5000 API root (discovery): http://localhost:5000/api API Reference: http://localhost:5000/swagger OpenAPI spec: http://localhost:5000/swagger/v1/swagger.json Rendered llms.txt: http://localhost:5000/llms

This document is the canonical orientation. Use GET /api/toc for the full endpoint catalog and GET /api/schemas/layer1 for request/response shapes.

OverMox is a realtime 3D creative environment. OverMox Controller is its companion visual node graph editor. You create graphs in the Controller, add nodes to them, connect node ports together, set values, and run the graphs to execute logic in OverMox. Think of it like building a program visually - each node is a function, connections are data/control flow, and ports are inputs/outputs.

Build and manage node graphs, nodes, connections, ports, and variables in the Controller. Create graphs, add nodes, connect ports, set values, run/stop execution.

  • Base: /api/graphs
  • Discovery: /api/node-types, /api/node-types/enum-values, /api/schemas/layer1

Execute OverMox capabilities as one-shot commands without building graphs. 161 actions across categories like scene, transform, physics, audio, animation.

  • Base: /api/actions/{category}/{action}
  • Discovery: /api/actions/schema
  • Key scene actions: scene.spawn (use ‘type’ for a built-in Element MenuPath from GET /api/discovery/spawnables, ‘sceneObject’ to duplicate by tag, ‘asset’ for a Bundled Asset or imported model by its exact assets.list name (an imported model’s name includes its file extension, such as Fox.glb) - exactly one required), scene.setColor, scene.switch, scene.reset, scene.clear (optional preserve parameter), scene.objects.destroy, scene.objects.setName, scene.objects.setActive, scene.objects.setLayer, scene.objects.getLayer, scene.createLayer (create or rename a layer by index; valid indexes 0-5, 9-15 and 25-31, while 6-8 and 16-24 are reserved). Layer names must be configured OverMox layers - use GET /api/discovery/layers to enumerate
  • Asset actions: assets.list (enumerate all project assets with optional filters), assets.delete (permanent deletion, bundled assets require confirm=true), assets.createBundled (save a scene object as a reusable bundled asset), assets.getProperty / assets.setProperty / assets.setProperties (read/write asset properties via actions), assets.getProperties (bulk-read all properties), assets.getShader / assets.changeShader (read or change shader on materials), assets.listShaders (discover available shader names)
  • Animation: play/pause/resume/stop/seek/setWeight/setSpeed/transition/playSet. Clips play CONCURRENTLY on one object, so run a gesture over a base loop by playing the base with loopMode loop, then playing the gesture with additive=true or a partial weight (author per-region clips for separate body parts to layer motions cleanly). An additive clip adds the delta from its own first frame each frame and stays bounded only while a lower non-additive clip keeps writing every property it touches; without that base the value accumulates without bound. play and playSet check this once at play time and return an advisory warnings array: code additiveNoBaseWriter names any affected property left without a base writer, and code additiveCoverageUnknown reports (with a reason and a clip or base scope) any source that could not be read, so an unreadable clip or base is surfaced rather than skipped and is never mistaken for a clean result. An unreadable base never suppresses additiveNoBaseWriter, so that list stays authoritative (possibly over-broad, never withheld) even when an additiveCoverageUnknown entry sits beside it; a base that stops later still re-exposes the drift silently. animation.playSet starts several clips atomically with per-item weight and additive. An optional layer on play and playSet reserves that exact blend layer, omitting it takes the lowest free one, and a layer another live playback already holds is refused with 409 LAYER_OCCUPIED with nothing evicted (playSet refuses the whole set), unless replaceGroup true (per item on playSet) licenses retiring a holder in the same non-empty group, which then stops with reason replaced while the new clip takes the layer in the same frame. group names on a play request scope later control calls (pause/stop/setWeight by group). Read live playback state via GET /api/objects/{tag}/animations/playbacks and static clip data via GET /api/objects/{tag}/animations/clips/{clip}.
  • MIDI: midi.send (output), midi.subscribe/unsubscribe (input streaming), midi.devices (enumeration)
  • Output Settings: output.get (read), output.set (write), output.toggle (individual booleans) - resolution, FPS, VSync, Spout, NDI, markup
  • Webhooks: webhook.listen (register), webhook.unlisten (remove), webhook.listeners (list) + webhook.received L3 event
  • Streaming: streaming.platforms (discover), streaming.subscribe/unsubscribe (control), streaming.status (list, with each subscription’s connectionState and connectionError) + streaming.event L3 event - supports StreamElements and StreamLabs, each through a pasted token. StreamElements takes the Overlay Token or the JWT (optional tokenType jwt or apikey, detected from the token when omitted) and an optional sourcePlatform filter (any, twitch, youtube, kick); its eventTypes cover alerts, tips, chat, stream status and counters

Stream events from OverMox and send continuous control inputs via SignalR at ws://localhost:5000/api/events. Events include graph.running, graph.stopped, node.executing, console.log, tween.completed, tween.cancelled, animation.playback lifecycle, midi.input, and more. Data-level subscription filters (SubscribeFiltered) narrow delivery by the event’s object, group, or clip on top of the type patterns.

  • Catalog: visit /asyncapi for the human-readable AsyncAPI viewer or GET /api/events/types for the raw JSON document.
  • node.done: a node finished executing.
  • ports.updated: port values changed, from a running graph or from a Layer 1 write (PUT /api/ports/{portId}/value).
  • variable.changed: a variable was created, or its value or name was updated (by a graph’s Create Variable or Set Variable node, the variables.create or variables.set action, arrays.create or an arrays.copy into a new target, POST /api/variables, or PUT /api/variables/{id}).
  • variable.deleted: a variable was removed.
  • webhook.received: an external HTTP POST hit a registered webhook listener.
  • animation.playback.started: an animation playback began on a scene object.
  • animation.playback.completed: a playback reached its natural end (reason natural).
  • animation.playback.stopped: a playback halted early (reason explicit, replaced, or sceneUnload).
  • animation.playback.looped: a looping playback completed a loop (full round trip under PingPong).
  • animation.playback.direction.changed: a PingPong playback reflected (direction forward or backward).
  • animation.playback.held: a ClampForever playback froze on its end pose.
  • animation.playback.paused: a playback was paused on its current frame.
  • animation.playback.resumed: a paused playback continued.
  • animation.playback.marker: a playing clip crossed a marker position attached to the play, playSet or transition request (payload carries the marker name and position).
  • streaming.event: a streaming.subscribe subscription delivered a StreamElements or StreamLabs event; a StreamElements payload carries its eventType’s fields, isTest (every eventType but chat, streamStatus and counter), sourcePlatform and raw.
  • process.loaded: a process started via process.start finished loading; payload carries processId, processName, and startTime. Subscribe and filter by processId from the process.start response to gate downstream work on a specific child.
  • process.exited: a process started via process.start exited; payload carries processId, processName, and exitCode. Pairs with process.loaded for full-lifecycle tracking; process.start is fire-and-forget on the REST side.
  • project.opened: a project was loaded.
  • project.closed: a project was unloaded.
  • scene.activated: a scene became the active scene.
  • scene.deactivated: a scene stopped being the active scene.
  • camera.changed: the active scene’s main camera changed.
  • overmox.connected: the OverMox runtime connected (or reconnected) to the Controller; payload carries the open project’s metadata (version, path, graphs, variables, restServerURL), and the envelope carries no data when no project is open.
  • overmox.disconnected: the OverMox runtime disconnected from the Controller; no payload, and SendAction and SendActionBatch fail until the next overmox.connected.
  • Hub methods: Subscribe(patterns), SubscribeFiltered(patterns, objects, groups, clips), Unsubscribe(patterns), SendAction(CommandRequest), SendActionBatch(commands, parallel)
  • SendAction: pass one object with action (dot notation, e.g. “log.console”), parameters, and optional requestId (omit it or send the empty GUID for fire-and-forget). Hub JSON is camelCase.
  • Hub commands get the same parameter check as the REST action routes (GET /api/actions/schema) before dispatch. A refused command is not dispatched: with a requestId the refusal arrives as an ActionResponse with success false and errorCode INVALID_REQUEST, and a fire-and-forget refusal fails the hub call with the same message.
  • SendActionBatch: pass the command list and a parallel Boolean; the hub requires both arguments, and a call with the command list alone runs no command. With parallel false the commands run one after another, and a command with a requestId has its response awaited before the next command is sent; with parallel true the commands are sent in order and their responses are collected concurrently. A command the parameter check refuses is listed in BatchActionResponse with its refusal.
  • Callbacks: ReceiveEvent, ActionResponse (per-command), BatchActionResponse (batch completion)

The external API is OFF by default; enable it under Preferences > API in the OverMox Controller. While disabled, gated endpoints return 403 API_DISABLED; the free endpoints, the doc viewers and the OverMox runtime link keep working. Gated endpoints require a token. For REST send it as the X-API-Key header; for the /api/events hub send it as the access_token query parameter or a Bearer header. The token (the ApiToken value) is auto-generated and stored in the per-user settings file at %AppData%/OverMoxController/api-security-settings.json, readable only by the current OS user. GET /api/health is always free and explains onboarding - it reports apiEnabled and authenticated, and for an unauthenticated loopback caller includes the settings-file path (auth.tokenFile). Tiers: free endpoints (no token) are /api, /api/health, /api/toc, /api/schemas, /api/actions/schema, plus the doc viewers (/swagger, /llms, /asyncapi, /llms.txt); everything else under /api and the hub SendAction/SendActionBatch methods are gated. Network: loopback is the default, serving plain HTTP on localhost that is not network-reachable. Optional LAN mode also serves each network IPv4 over HTTPS with a self-signed cert (its SHA-256 fingerprint is shown for client pinning); gated endpoints and the /api/events hub still require the token and answer only loopback and allow-listed client IPs (a REST call from any other client gets 403 CLIENT_NOT_ALLOWED and a hub connection is refused), while the free endpoints and the doc viewers answer any LAN client. CORS: cross-origin browser access is intentionally blocked - the server only reflects loopback origins (localhost / 127.0.0.1, any port), never *, and the same policy applies to free and gated endpoints, both network modes, and the /api/events hub (which also re-checks Origin at connect, since CORS does not cover WebSocket upgrades). Non-browser clients (curl, scripts, AI agents) are unaffected by CORS and only need the token. The internal OverMox runtime link is loopback-only and Origin-checked, takes no token, and is unaffected by the API toggle. Sensitive port values (JWT tokens, API keys) are redacted by default. Port responses include isSensitive (Boolean) and hasValue (Boolean, when redacted) metadata. Writing to sensitive ports always works, but every response and ports.updated event that carries the value redacts it, the 200 response of PUT /api/ports/{portId}/value included. Enable ‘Expose Sensitive Port Values via API’ under Preferences > API in the OverMox Controller for full access.

Query what OverMox knows at runtime - scene objects, Bundled Assets, components, and more.

GET /api/discovery - list all 11 available data types with descriptions GET /api/discovery/scene-objects - all objects in the current scene GET /api/discovery/bundled-assets - available Bundled Assets for spawning GET /api/discovery/runtime-assets - audio clips, videos, materials, textures, models GET /api/discovery/components-info - component types and their properties GET /api/discovery/scenes - available scenes GET /api/discovery/transitions - animation transitions GET /api/discovery/midi-devices - connected MIDI hardware GET /api/discovery/input-info - input bindings (no OverMox needed) GET /api/discovery/input-action - current input state GET /api/discovery/app-data - application data and component catalog GET /api/discovery/layers - configured OverMox layers (names for SetLayer / Other Layer)

All responses use uniform envelope: { “dataType”: “…”, “count”: N, “items”: […] } Use ?filter=keyword to narrow results by name/type.

Read and write component and asset properties directly on scene objects and assets, without building node graphs.

Workflow: discover components on an object, then list or read/write individual properties.

GET /api/objects/{tag}/components - list all components on a scene object with property counts GET /api/objects/{tag}/components/{component} - list all properties on a component with current values and types GET /api/objects/{tag}/components/{component}/{property} - read a single property value PUT /api/objects/{tag}/components/{component}/{property} - set a property value. Body: { “value”: }

GET /api/assets/{tag}/properties - list all properties on an asset with current values GET /api/assets/{tag}/properties/{property} - read a single asset property value PUT /api/assets/{tag}/properties/{property} - set an asset property value. Body: { “value”: }

Set multiple properties in one call instead of N individual PUTs. Best-effort - each property is set independently, and per-property results are returned.

PUT /api/objects/{tag}/components/{component}/properties - set multiple component properties POST /api/actions/components/refresh - trigger visual rebuild after property changes (AudioVisualizer, DynamicShadowBoxModeler, VectorShape, TextElement3d) PUT /api/assets/{tag}/properties - set multiple asset properties

Body: { “properties”: [ { “name”: “Position”, “value”: { “x”: 1, “y”: 2, “z”: 3 } }, { “name”: “Scale”, “value”: { “x”: 2, “y”: 2, “z”: 2 } } ] } Response: { “totalCount”: 2, “succeededCount”: 2, “failedCount”: 0, “results”: [ { “name”: “Position”, “success”: true }, … ] }

  • Property names are case-sensitive and may contain spaces - URL-encode them (e.g., Is%20Trigger).
  • Use GET /api/discovery/scene-objects to find valid object tags.
  • Use GET /api/discovery/runtime-assets to find valid asset tags.
  • Use GET /api/types for value format reference when setting property values.

Read the in-memory console log buffer (up to 500 entries, newest first).

GET /api/console - list all console log entries GET /api/console?type=error - filter by severity (log, warning, error) GET /api/console?contextType=graph - filter by context kind (graph, node, none) GET /api/console?since=2024-01-15T10:30:00Z - entries after a timestamp GET /api/console?type=error&limit=10 - combine filters

For real-time console streaming, subscribe to the console.log WebSocket event via Layer 3.

GET /api/health Returns: { “status”: “ok”, “overmoxConnected”: true } Both OverMox and the Controller should be running. Layer 2 and 3 require OverMox to be connected (check overmoxConnected in the health response).

GET /api/toc Returns the full endpoint catalog with method, path, and one-line purpose for every endpoint. Combine with this guide for orientation.

GET /api/node-types/search?q=console log Returns matching nodes with their enumValue field. The enumValue is what you use as the category when adding nodes.

Currently available: 170 nodes across 20 categories. GET /api/node-types/enum-values returns all valid category values.

POST /api/graphs Body: { “name”: “My Graph” } Both fields are optional - name is auto-generated if omitted, path defaults to the project’s Graphs folder if omitted. Returns 409 NO_PROJECT if no project is open and path is omitted, 409 GRAPH_NAME_EXISTS if a graph by that name already exists, or 409 DUPLICATE_PATH if the path is another graph’s or holds a file no graph in the project uses (a graph file one POST /api/graphs/{id}/remove left on disk, for example); nothing is written over it. Use ?replace=true to replace an existing graph with the same name instead of getting 409 GRAPH_NAME_EXISTS: it leaves the project as POST /api/graphs/{id}/remove takes a graph out, and its file is written over only when the new graph goes to the same path. Returns the full graph detail including two default nodes (Start and OnNewFrame) with all their port IDs - no follow-up call needed.

POST /api/graphs/{graphId}/nodes Body: { “category”: “ConsoleLog”, “locationX”: 200, “locationY”: 0 } Use the enumValue from step 3 as the category field. The response includes the node’s ID and all its input/output ports with their IDs and types.

For bulk creation, POST /api/graphs/{graphId}/nodes/bulk with an array of node specs (up to 1000). All-or-nothing with rollback.

POST /api/graphs/{graphId}/connections Body: { “outputPortId”: “…”, “inputPortId”: “…” } Connect an output port from one node to an input port on another. Port IDs come from the node responses in step 5.

PUT /api/ports/{portId}/value Body: { “value”: “Hello World” } Use GET /api/types to learn the correct value format for each port type (e.g., Vector3 uses {“x”:0,“y”:0,“z”:0}). Vector ports also accept [x,y,z] arrays or a scalar (applied to all axes). Color ports accept [r,g,b,a], [r,g,b] (alpha defaults to 1), or {“r”:1,“g”:0,“b”:0,“a”:1}. For dropdown/enum ports, use GET /api/ports/{portId}/options to discover valid values. Set by label string or index integer. Invalid strings return 400 with valid options listed.

POST /api/graphs/{graphId}/sync (required before run - L1 edits do not auto-sync, while removals and deletes reach OverMox at once; loads this graph alone and leaves every other graph as it was) POST /api/graphs/{graphId}/run (execute the graph) POST /api/graphs/{graphId}/stop (stop execution) POST /api/graphs/{graphId}/ports/sync (force-flush live port values during execution; pair with GET /api/ports/{portId}/value for deterministic reads while the graph runs) Tip: All L1 mutation endpoints accept ?sync=true to sync after the mutation (e.g., POST /api/graphs/{graphId}/nodes?sync=true). Use on the last edit in a batch instead of a separate /sync call. A sync pushes only the graphs with edits OverMox does not have yet, each alone, and stops no running graph. A running graph’s push waits until it stops (POST /api/graphs/{graphId}/sync then answers synced false), and the stop delivers it.

POST /api/project/open (open a project from a file path - requires OverMox connected; returns 403 ACCESS_REQUIRED while OverMox has no access, so sign in or subscribe in OverMox before retrying; returns 409 ACCESS_CHECKING while access is being checked, so retry after a short wait) POST /api/actions/project/refresh (refresh the project panel from disk - requires OverMox connected) POST /api/graphs/{graphId}/save (save to disk - returns 409 GRAPH_RUNNING if running; stop it first) POST /api/graphs/{graphId}/duplicate (duplicate - returns 409 GRAPH_RUNNING if running; stop it first) POST /api/graphs/{graphId}/remove (remove from Controller - does NOT delete the file on disk)

GET /api/nodes/{nodeId} - look up a node by ID across all graphs. Returns node detail with a graphId field. No graph ID needed in the URL.

Note nodes (category=Note) are visual annotations rendered in the graph canvas. They have header-only formatting (font, color, font size) that does not affect graph execution. Text body still uses the standard PUT /api/ports/{portId}/value path; the formatting fields are exposed only via the dedicated sub-resource:

GET /api/graphs/{graphId}/nodes/{nodeId}/note - returns the current text, font, color, and fontSize plus availableFonts/availableColors/availableFontSizes for discovery. PUT /api/graphs/{graphId}/nodes/{nodeId}/note - partial update. Body: { “text”: “…”, “font”: “…”, “color”: “…”, “fontSize”: 20 }. Any subset of fields; absent fields stay unchanged. Unknown font/color or unsupported fontSize returns 400 INVALID_VALUE; calling against a non-Note node returns 400 INVALID_REQUEST.

POST /api/graphs/{graphId}/build - create nodes, set port values, and wire connections in one call. Supports connections-only builds (empty nodes array) for wiring existing nodes. All-or-nothing with rollback on failure. Limits: 1000 nodes, 5000 connections.

Body: { “nodes”: [ { “tempId”: “n1”, “category”: “ConsoleLog”, “ports”: { “Message”: “Hello” } } ], “connections”: [ { “from”: “n1/Exit”, “to”: “existingNodeId/Enter” } ] }

Connections reference ports as tempId/PortName (new nodes) or existingNodeId/PortName (existing nodes). Port values accept compound formats (arrays, objects, scalars for vectors and colors).

POST /api/batch - execute multiple operations in one request. Best-effort, sequential.

Body: { “operations”: [ { “method”: “POST”, “path”: “/api/graphs”, “body”: { “name”: “…” } }, … ] } Response: { “totalOperations”: N, “succeeded”: N, “failed”: N, “results”: [ { “index”: 0, “status”: 201, “body”: … } ] }

Order preserved. Each runs through full middleware. No recursive batches.

  • Nodes are functions. Each has typed input and output ports.
  • Connections link an output port to an input port, creating data or control flow. Port type compatibility is validated - allowed pairings: same type, Generic with any, Float with Integer, String with ObjectSelector, Button to Trigger (one-way). Use POST /api/graphs/{graphId}/connections/validate to check before connecting.
  • Port types use shortened canonical names: Float, Trigger, Dropdown. Use GET /api/types for the full list.
  • Trigger ports control execution order (like trigger/execution pins in visual scripting).
  • Category enum values are the canonical identifiers for node types. Always use GET /api/node-types/enum-values to discover valid values.
  • Port expansion - Vector and Color ports can be expanded into individual sub-ports (X/Y/Z or R/G/B/A) via POST /api/ports/{portId}/expand and collapsed via POST /api/ports/{portId}/collapse. Port responses include metadata: isExpandable, isExpanded, isExpandedChild, parentPortId, affectsNodePorts.
  • Trigger port disable/enable - Trigger ports can be disabled via POST /api/ports/{portId}/disable and re-enabled via POST /api/ports/{portId}/enable. Both are idempotent. Returns 404 PORT_NOT_FOUND, 405 TYPE_IMMUTABLE for non-trigger ports, or 409 GRAPH_RUNNING if the graph is running.
  • Dynamic ports - Dropdown/enum port value changes may alter the node’s port structure. When this happens, PUT /api/ports/{portId}/value returns both the port and the full updated node. Check affectsNodePorts on a port to know if it may trigger structure changes.
  • L2 parameter validation - Layer 2 action parameters are validated against schemas before dispatch. Wrong types or missing required params return 400. Use GET /api/actions/schema for parameter details.
  • Layer 2 parameters are case-insensitive - both {“text”: “hello”} and {“Text”: “hello”} work. Use GET /api/actions/schema for canonical parameter names.
  • Variable sync for L2 actions - if you update a variable’s value (PUT /api/variables/{id} with a value in the body) and then call an L2 action that reads it (e.g., text.format), add ?sync=true to the variable update so the new value reaches OverMox before the action runs.
  • Camera control - cameras are scene objects. Use transform/tween actions with the camera’s tag (e.g., _camera) for dolly shots, rotations, look-at, and orbit. Use the property endpoint to adjust Field Of View for zoom.
  • ComposeVector3 - use the ComposeVector3 node to build a Vector3 from individual X, Y, Z float values. Useful when constructing positions or rotations from separate numeric inputs.

Many error messages name the endpoint to call to fix the problem. For example, an invalid category error will tell you to check GET /api/node-types/enum-values.

Error format: { “error”: { “code”: “ERROR_CODE”, “message”: “Human-readable message with fix suggestion” } } Full error catalog: GET /api/errors

Project opening: while a project open is in flight, OverMox refuses every Layer 2 action in the scene, camera, transform, viewport, world, tween, physics, animation, shatter, components, properties, assets, audio, particles and project categories before it runs. POST /api/actions/{category}/{action} answers 409 PROJECT_OPENING, and a hub command sent with a requestId gets a response with success false and errorCode PROJECT_OPENING. Wait for the project.opened event, then retry; a brief retry on a repeat 409 is also safe. Actions in the other categories (math, text, log and the rest) run as usual.

  • All list endpoints support ?limit=N&offset=M for pagination. X-Total-Count header is always present.
  • Use ?wrap=true to get a {totalCount, items} envelope instead of a bare array.
  • Some endpoints support ?include=field1,field2 or ?exclude=field1,field2 to reduce response size.

6 endpoints under /api/graphs/{id}/layout/ for auto-layout, tidy, restore, and aspect ratio management:

  • POST /api/graphs/{id}/layout - Apply auto-layout (7 modes: Story, Compact, Wide, etc.)
  • POST /api/graphs/{id}/layout/tidy - Grid-snap and alignment
  • POST /api/graphs/{id}/layout/restore - Restore pre-layout positions
  • GET /api/graphs/{id}/layout/saved - Check if saved positions exist
  • GET /api/graphs/{id}/layout/aspect-ratio - Get current aspect ratio
  • PUT /api/graphs/{id}/layout/aspect-ratio - Set aspect ratio
Endpoint Purpose
GET /api/node-types Browse node categories
GET /api/node-types/search?q=… Search for nodes by name
GET /api/node-types/enum-values All valid category values for adding nodes
GET /api/schemas/layer1 Request/response schemas for CRUD operations
GET /api/actions/schema All Layer 2 action schemas with parameters
GET /api/types Port types, compatibility rules, value formats
GET /api/errors Error code catalog with resolutions
GET /api/health API and OverMox connection status
GET /api/toc Complete endpoint listing
GET /api/objects/{tag}/components List components on a scene object
GET /api/assets/{tag}/properties List properties on an asset
PUT /api/objects/{tag}/components/{component}/properties Set multiple component properties in one call
PUT /api/assets/{tag}/properties Set multiple asset properties in one call
GET /api/console Console log history with filtering
GET /api/nodes/{nodeId} Look up a node by ID across all graphs
POST /api/graphs/{id}/nodes/bulk Add multiple nodes in one call (up to 1000, all-or-nothing)
POST /api/graphs/{id}/build Declarative graph builder (nodes + ports + connections in one call)