Persona runs a local WebSocket server that lets other applications control Persona: switch scenes, add and place layers, toggle expressions, play motions, play lip-synced speech, read tracking status, and write model parameters directly
It is off by default. Turn it on under Settings → Plugin API, create a key, and paste the token into your plugin
Turning It On
| Control | Effect |
|---|---|
| Enable API | Starts the server. Nothing connects without a key |
| Port | Default 25034 |
| Allow Access on Local Network | Binds beyond loopback, so a phone or another machine can reach it |
With local network access on, Persona shows the addresses you can use to connect from a browser
If the port is already taken, the section shows the error instead of failing silently
API Keys
A key is required — an unauthenticated connection is refused. Create Key names one and shows its token once in full; the copy button on its row reveals it again at any time
| Action | Effect |
|---|---|
| Copy Token | Copy the full token to the clipboard |
| Rename Key | Rename it in place |
| Revoke Key | Delete it. Sessions using it are closed and told not to reconnect |
Each key row also reports Connected, Never used, or a relative Last used … time. Once a client identifies itself, the row remembers its latest name and version, so the key remains recognizable while no client is connected
Tokens are held in the operating system's secure storage. On a machine where that is unavailable, key creation fails with an explanation rather than writing a token to a plain file
Plugin Folders
A plugin can ask Persona to register a model or asset file, so it can then load it. That only works for files inside a folder you added under Plugin Folders — any other path is refused with forbidden-path
Connected Clients
Every open session is listed while the server is running, by the name it declared, with its version, developer, which key it used, and where it connected from. Browser clients show their page origin
A client that never identifies itself is listed as Unidentified client and works exactly the same. The disconnect button on a row closes that session and instructs the client to stop reconnecting
Treat a token like a password. It grants control of the application, including loading any model or asset registered from your plugin folders. The server listens on loopback only unless you turn on local network access
The Web Console
persona.laplace.live is a browser console built on this API. As a first-party “plugin”, use it to check the connection or control Persona from a phone or tablet during a stream
Enter the host, port and key to control scenes, layers, expressions, motions, effects, the camera, lighting, shortcuts, tracking and settings. The layout follows the desktop Control Panel: layers appear on the left, with Stage, Tracking, Shortcuts and Settings below them. The selected item’s details appear on the right when space allows. Each section has a URL you can bookmark. Select a model to access its expressions, motions and Parameters bindings, or an effect in the layer list to adjust its parameters. Controls are disabled when the selected instance does not support them
The Add to Scene button below the layer list opens the Inventory, populated from Persona’s registry through the API. To register a file from the console, enter its path on the desktop machine. The file must be inside a configured Plugin Folder
The scene assets include separate Environment Maps and LUTs tabs. Click an asset to apply it to the current scene; a toast confirms the change. Animations has no separate tab because the API does not support direct clip playback. Register a clip using the kind picker on the All tab, then use Choose… to set it as a layer’s idle animation
Select the scene and Lighting lists the lights a 3D object or environment carries, with the same switch and intensity slider as the desktop. Loaded asset dimensions and glTF extensions appear there too
You can store the key in the browser without encryption or keep it for the current session only. Control requests go directly to Persona on your machine or local network
For an unencrypted ws:// connection from the hosted HTTPS console, use
127.0.0.1 to connect to Persona on the same machine as the browser. To reach
one on another device, serve the console over plain HTTP, or put a TLS proxy
in front of Persona and turn on Use TLS (wss://). A first connection may
also ask for local network permission
The following features are available only in the desktop application: the updater, API keys, the tray icon, window bounds, native file pickers, on-stage handles and shortcut recording. App and scene shortcuts are created on the desktop too — the console can only fire them
Stream Deck
The Stream Deck plugin is the other first-party client of this API, letting you use physical keys to switch models and scenes, trigger hotkeys, expressions and motions, and move or scale the avatar
The SDK
@laplace.live/persona-sdk is a typed client that works in Node, Bun, browsers and OBS browser sources
import { PersonaClient } from "@laplace.live/persona-sdk";
const persona = new PersonaClient({ token: "sk-lp-v1-…" });
await persona.connect();
await persona.call("expression.toggle", { name: "Smile" });
persona.on("motion.started", (m) => console.log("playing", m.group));Every method is typed through one shared table, so a call's request and response shapes follow from its name. The SDK reference covers installation, client options, authentication, parameter leases and every exported type
Events
A session receives only the events it subscribes to. The SDK restores subscriptions after reconnecting
| Event | Fires when |
|---|---|
scene.changed | Any scene mutation: create, delete, rename, activate, or an edit |
scene.loading | A scene apply or pipeline warm-up starts and finishes |
settings.changed | Application settings changed |
instance.loaded | A model instance finished loading on stage |
selection.changed | The editing selection moved, from any source |
expression.changed | The active expression set changed |
expression.persistence | A model's Remember Expressions flag was toggled |
motion.started | A motion began playing |
motion.ended | A motion finished |
speech.started | A speech.play utterance began |
speech.ended | It finished, was stopped, or was superseded |
hotkey.state | The hotkey configuration changed |
shortcut.state | The global shortcut list, its names, or its OS registrations changed |
tracking.status | Face or pose tracking status changed |
registry.changed | A model or asset was registered — re-read the matching list |
Methods
Model-scoped methods take an optional instanceId and default to the scene's primary model
| Group | Methods |
|---|---|
scene.* | list get activate create duplicate rename delete setShortcut patch inspect |
instance.* | list get add setModel remove setVisible setTrackingSources reorder setPrimary setPlacement setMToon setIdle setControllerMovement info |
binding.* | get preview set release inputs override |
controller.* | state rename setDeadZone remove assign |
selection.* | get set |
object.* | add rename setContent setSpace setPlacement attach setLightOverrides anchors |
expression.* | list active toggle getPersistence setPersistence |
motion.* | list play playing |
speech.* | play stop |
hotkey.* | list set trigger |
shortcut.* | list trigger |
model.* / asset.* | list and register for each, plus registry.thumbnail |
settings.* | get patch |
tracking.* / pose.* | tracking.addSource tracking.updateSource tracking.removeSource, setEnabled, setSource, plus pose.setPort and tracking.status |
stage.* | resetTransform resetCamera |
app.* / session.* | app.info app.localAddresses app.stats, and session.identify |
storage.* | get set delete list |
param.* | inject release |
Capabilities
Check the application and instance capability lists to determine which features are supported, rather than comparing version numbers
The application's own is reported in hello and by app.info — currently storage, speech, shortcuts, controllers, model-editing and asset-inspection. Each instance carries its own under capabilities in instance.list, and a method no capability covers is refused with unsupported-for-format
The last three each unlock a set of shared-editor methods. controllers covers controller.* — reading connected devices and saved profiles, renaming, setting a dead zone, removing one, and starting an assignment — plus the controller.enabled master switch through settings.patch; the devices are the ones attached to the desktop, not to the browser's own computer. model-editing covers binding.* (reading, previewing, committing and releasing a model's parameter bindings, and short-lived input overrides) along with instance.setBreath. asset-inspection covers scene.inspect and registry.thumbnail
instance.setControllerMovement merges an instance's controller movement configuration: enabled, slot (a saved profile number), moveSpeed, turnSpeed, smoothing, and on VRM also walkEnabled, walkClip, walkSpeed and faceMovement. Out-of-range values are reset to defaults or clamped to the allowed range. Calls targeting objects return unsupported-for-format. walkClip accepts the same references as instance.setIdle
Tracking Sources
Persona can run several configured tracking sources at once. settings.tracking.sources lists them, while tracking.addSource, tracking.updateSource and tracking.removeSource manage the list. instance.setTrackingSources binds an instance's face and pose channels (faceSourceId and poseSourceId) by source id. null stops tracking for that channel, and an unknown id behaves the same. New instances start on each channel's default source; one source may drive several instances
Face sources use persona-ios, ifacialmocap or vts-ios, with an optional phoneIp to pin one sender. Body sources use vmc or mocopi. A port belongs to vmc, mocopi and ifacialmocap — one already held by another source is rejected, except between ifacialmocap sources, which share a socket; persona-ios and vts-ios have their ports fixed by the protocol. The retired vts-ios-native kind is rejected on the wire
tracking.status returns the aggregate face and pose state plus a sources map with every configured source's own status; the tracking.status event carries the same shape
The legacy single-source methods still address the first source in their channel: tracking.setSource changes its kind, pose.setSource creates a VMC source when needed and returns its port, and pose.setPort updates that port. tracking.setEnabled and pose.setEnabled remain the channel-wide master switches
Speech
speech.play plays audio and drives the mouth from its loudness. It works on instances advertising the speech capability — which follows the rendering engine rather than the format, so check the instance's capabilities. Today Live2D on the default engine advertises it; the legacy WebGL mode does not
| Parameter | Meaning |
|---|---|
url | https:, http: (a local TTS bridge), or an inline data:audio/* payload |
volume | Optional, 0..1 |
instanceId | Optional, defaults to the scene's primary model |
One utterance per model: a new play supersedes the current one. The call returning means playback started; invalid-state means it did not. speech.started and speech.ended bracket the utterance, and speech.stop ends it early
Global Shortcuts
A build carrying the shortcuts capability can list and run the application's app shortcuts — macros that apply several stage changes together
| Method | Effect |
|---|---|
shortcut.list | List every shortcut |
shortcut.trigger | Run a shortcut by its ID |
Action payloads stay app-side. What the list gives you is id, title, accelerator, registered, and an actionKinds array — enough to draw a row of buttons without knowing what each action does. When title is null, derive a label from actionKinds; the SDK's shortcutLabel does exactly that
accelerator can also be null. That is not an error, only a shortcut with no combination bound — shortcut.trigger still fires it. Whenever the list, its names, or its OS registrations change, shortcut.state delivers the whole list again
actionKinds is open-ended: a newer build may send kinds this SDK has no name
for. Treat an unknown one as unknown rather than as an error —
shortcutActionLabel folds them under a generic name
Plugin Storage
storage.* gives a plugin durable key–value storage namespaced by API key — the key is the plugin's identity, every session on it shares the same data, and revoking the key deletes it. Values are arbitrary JSON
| Method | Effect |
|---|---|
storage.get | Read a key. An unset key returns value: null |
storage.set | Write a key |
storage.delete | Delete a key. An absent key still succeeds |
storage.list | List every key, sorted |
The limits are 128 characters per key, 64 KB per serialized value, and 256 keys per API key. A storage.set call that would exceed the key count limit returns invalid-state
Errors
persona.call throws PersonaApiError with a string code:
| Code | Meaning |
|---|---|
parse-error | The frame is not valid JSON |
invalid-request | The envelope was malformed |
unknown-method | This build has no such method |
invalid-params | The parameters failed validation |
not-found | A referenced id resolves to nothing |
unsupported-for-format | The instance's format cannot do this — MToon on a Live2D model, say |
conflict | Another session holds the parameter lease |
renderer-unavailable | The stage could not answer — no window, or a reloading renderer |
forbidden-path | The path is outside every configured plugin folder |
invalid-state | The operation cannot complete: deleting the last scene, exceeding a storage quota, or failing to start audio |
internal | Everything else |
The code set is part of the protocol: clients drop error frames carrying an unknown code, so new codes only arrive with a protocol bump and new failures reuse these
Disconnects
The client automatically reconnects after an unexpected disconnect. The following close codes stop automatic reconnection to respect key revocation or a manual disconnect:
| Code | Constant | Cause |
|---|---|---|
4001 | CLOSE_KEY_REVOKED | The key was revoked |
4002 | CLOSE_FORCE_DISCONNECTED | The user disconnected the session |
Both settle the client in the closed state. If onClose is supplied, a terminal close is delivered there and onWarning is skipped; without onClose, it falls back to onWarning. 1001 is not one of them — it means the server is stopping, so reconnect once it returns
Revoking a key immediately discards pending frames and closes connections using that key with 4001. Requests still waiting for the stage cannot resume writing. Actions already sent to the stage may finish, but their responses are discarded
Version Mismatches
The protocol carries a version, and the SDK warns when it does not match the connected build. Subscribing to an event an older Persona does not know is not fatal — the SDK falls back to subscribing one event at a time, keeps the ones that work, and warns by name about the rest
Last updated on September 10, 2026