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
Every accepted connection also posts a silent system notification naming the client, the key it used and where it connected from, so a key in the wrong hands shows up as it is used. Where the operating system refuses notifications — an unsigned development build, a denied permission — the same notice appears as a toast in the Control Panel while that window is open. It never lands on the stage, which a capture would carry
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-console.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, automations, tracking and settings. The layout follows the desktop Control Panel: layers appear on the left, with Stage, Tracking, Automations 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 under Scene Effects to adjust its parameters; a selected model or 3D object also carries its own Layer Effects. 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. Automations are authored on the desktop too: the console's Automations page lists and runs them, with the primary avatar's shortcuts in a collapsible Avatar Shortcuts section below. An automation paused on the desktop is marked Paused and cannot be run
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
MCP Server
@laplace.live/persona-mcp is the third: a Model Context Protocol server that hands this API to an AI assistant — Claude, Cursor, Codex — as around twenty task-shaped tools, plus call_api for everything else and stage.capture so the assistant can look at what it did
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; started carries oneShot on hosts with motion-stop |
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 |
automation.state | The automation list, names, trigger kinds, enabled state or OS registrations changed |
tracking.status | Face or pose tracking status changed, or any one source's status — webcam face, hand and body tracking included |
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 patch inspect setLightFollowCamera |
instance.* | list get add setModel remove setVisible setEffects setTrackingSources reorder setPrimary setPlacement attach setMToon setIdle setControllerMovement setLipSync info |
binding.* | get preview set release inputs override |
controller.* | state rename setDeadZone remove assign |
selection.* | get set |
object.* | add addMany rename setContent setSpace setPlacement attach setLightOverrides anchors |
expression.* | list active toggle getPersistence setPersistence |
motion.* | list play playing stop |
speech.* | play stop |
hotkey.* | list set trigger |
automation.* | list run |
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 |
lipSync.* | state configure restart calibrate |
stage.* | resetTransform resetCamera spawn clearSpawned capture |
app.* / session.* | app.info app.localAddresses app.stats, and session.identify |
storage.* | get set delete list |
param.* | inject release |
scene.create appends an empty scene and activates it, as creating one in the application does, so the reply's activeSceneId is the new scene — read the id there rather than diffing scene lists, where another session's create could land in between
scene.patch edits the active scene's background, camera, lights, environment and transition; scene items go through instance.* and object.* instead. A light whose enabled is false is muted along with its shadows but keeps the rest of its settings. The field defaults to true, and builds that cannot switch lights off omit it
vrmCamera is replaced whole: orbit, fov (1 – 179°), roll (radians around the view axis), projection (perspective or orthographic) and clipAssetId. Older builds omit roll and projection, which read as 0 and perspective, so check the returned camera before offering them. The orbit's angles take any value in radians and its distance may be negative, while mouse navigation keeps its own narrower limits. An orthographic view is abs(distance) wide, keeps fov without using it, and flips both axes when distance is negative. stage.resetCamera resets roll and projection too
object.add takes an optional place2d or place3d, so the object lands at that placement in the same edit instead of showing a frame at the default one; values are clamped as in object.setPlacement. object.addMany { objects } adds up to 100 of the same entries as one scene edit — one save and one undo step — and answers instanceIds in request order. If any asset fails to resolve, nothing is added
instance.attach { instanceId, attach } pins a Live2D instance to another Live2D model as an item, taking the same attach as object.attach; null detaches it where it stands. attach.depth is { kind: 'front' }, { kind: 'behind' } or { kind: 'artMesh', id }, the last slotting the item in just above that ArtMesh of the parent — an id the parent lacks reads as in front. attach.split, { itemArtMesh, depth }, cuts the item at one of its own ArtMeshes and sends everything under that to a second depth. object.anchors rows carry order, the ArtMesh's paint order when the list was taken, which is how a depth control lays out its stops. A pin that would close a circle answers invalid-params, and a VRM instance answers unsupported-for-format. For an object, only front takes effect today
stage.capture needs the stage-capture capability and answers { dataUrl, width, height }: the stage window as displayed, a PNG with alpha, shrunk so its longest edge is at most maxEdge (64 – 2048, default 1024) and never upscaled. It is how a client that cannot see the screen — an AI assistant over the MCP server — checks its own edits. Each capture stalls the renderer for a frame, so read on demand rather than polling. renderer-unavailable means there was no stage window or no frame to copy; a crashed stage answers it until its reload has drawn, so retry rather than give up
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, automations, layer-effects, scene-transitions, area-lights, spot-lights, camera-follow-lights, shadow-filters, environment-map-model, spawn, tracking-lost, motion-stop, stage-capture, 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; binding.inputs answers { inputs, outputs }, where inputs are the live tracking inputs and outputs is what each bound parameter reads on the model right now, keyed by output id — after motions, expressions and physics, with anything unreadable omitted. 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
Motions and Idle
motion.list reports the model's motion groups. A Live2D model also lists every .motion3.json in its folder that its model3.json does not declare, each as a group named by the file — which is how a motion recorded in the desktop's Motions section shows up without a reload
motion.stop (the motion-stop app capability) fades the running motion out and lets the idle resume. It leaves the model alone and answers { stopped: false } when only the idle is playing. motion.playing's oneShot — also on motion.started — says whether the reported motion outranks the idle, which is what a stop would end; older builds omit the field
instance.setIdle merges an instance's idle settings:
| Field | Meaning |
|---|---|
idleAnimation | The idle master switch. false stops the VRM clip, and on Live2D every idle motion, the model's own Idle group included |
idleClip | VRM only. The idle clip's reference |
idleMotion | Live2D only, needs the idle-motions instance capability. A motion file from motion.list, looped beneath every other motion; null plays the model's Idle group at random. A file the model no longer lists behaves as null |
trackingLostBehavior | Needs tracking-lost. hold keeps the last tracked pose; idle eases back to the idle over ~0.3 s and in again when the face returns |
trackingLostMotion | Needs tracking-lost, Live2D only. The motion that replaces the idle once the face has been gone long enough; null keeps the idle |
trackingLostDelay | Needs tracking-lost. How long that is, in seconds, 0 – 60 |
instance.get reports all of these
Lighting
A build with the area-lights capability accepts a SceneLight of type area: a rectangle at x/y/z, aimed by azimuth/elevation in degrees, turned within its own plane by roll (−180 – 180°, default 0) and sized by width and height (0.01 – 20 scene units each, default 1). It lights PBR materials and MToon avatars, which approximate it with four light samples through their toon shading, but not Live2D, and it casts no shadows. Its range and shadow settings are kept for a later type change
A build with spot-lights accepts type spot, which uses x/y/z, azimuth/elevation and range (0.1 – 20 scene units). At zero azimuth and elevation the beam points along −Z, and positive elevation aims it downward. angle is the cone's half-angle (1 – 89°, default 30) and penumbra softens its edge (0 – 1, default 0.3). Intensity runs 0 – 20, as on a point light. A spot light lights PBR and MToon materials, takes the usual shadow quality and radius, and can light volumetric haze. Offer area and spot lights — as new lights or as type changes — only on builds with their capability
shadowFilter selects pcf (the default) or pcss for each directional, point or spot light, and both can share a scene. shadowQuality still sets the map resolution, and off still disables casting. PCF softens by shadowRadius, in shadow-map texels. PCSS searches for blockers and widens the penumbra with distance from the caster, according to shadowSourceSize: the source's diameter in degrees on a directional light and in metres on point and spot lights (0 – 10, default 0.1), where 0 is a hard source. PCSS leaves the saved shadowRadius alone, and older scenes load as PCF. Gate filter selection on the shadow-filters capability — a light created locally can carry shadowFilter even when the build does not support it
A build with the camera-follow-lights capability accepts followCamera on every light but ambient, which ignores it. It defaults to false; when true, x/y/z and the angles are relative to the camera, so the light moves with manual camera edits and with camera motion playback. followCameraOptions chooses what it follows, each true by default: position the camera's target and pan, rotation its world rotation including roll, and distance the signed orbit distance. Changing only fov never moves the light. A component switched off uses followCameraReference instead — targetX, targetY, targetZ, distance and a world-space rotation quaternion — frozen when it was switched off; a missing reference reads as a zero target, identity rotation and zero distance
scene.setLightFollowCamera { lightId, followCamera, options? } switches one light of the active scene, or merges options into its choices, without moving or re-aiming it: the desktop converts the light's coordinates against the camera on screen, camera motion included, and returns { light } with the rebased settings, or light: null if the light is gone. scene.patch and saved scenes take light values as written, with no such conversion. Gate the fields and the method on camera-follow-lights
SceneLight.volumetric switches illuminated haze on for one point, spot or area light. It defaults to false, and other types ignore it. Older builds omit it and have one scene-wide switch instead, so check for the field before offering the control; a scene saved with that switch on loads with volumetric on for every light that can haze
SceneEnvironment adds two optional fields, volumetricLighting and clusteredLighting. Older builds omit them, so check for them before offering either control. volumetricLighting sets how the haze looks across the scene and has no switch of its own — with no enabled light opted in, there is no haze. Current builds return the complete haze settings and heal clusteredLighting to off; edit both through scene.patch with the environment
volumetricLighting field | Values | Default |
|---|---|---|
density | 0 – 10 | 1 |
intensity | 0 – 10 | 1 |
range | 1 – 100 scene units from the camera | 20 |
quality | low, medium, high | medium |
speed | 0 – 5; 0 stops the haze moving | 0.2 |
The haze glows around the lights that opt in, occluded by depth and transparent where unlit, and while any enabled scene light opts in, the point, spot and area lights built into 3D objects and the environment model join it. It needs a perspective camera: an orthographic projection keeps the settings but renders no haze. Directional and ambient lights take no part, and Live2D does not occlude it. clusteredLighting: true speeds up surface lighting on WebGPU perspective cameras with 1 – 64 finite-range point lights that cast no shadows. A point light with no range, more lights than that, or another renderer or camera keeps conventional lighting. It does not reduce the cost of rendering shadows or change the lights the haze uses
On a build with environment-map-model, SceneEnvironment can carry iblAssetId and modelAssetId together. bakeFromModel then selects lighting generated from the model, which the map feeds; false lights with the map directly, falling back to the model's own sky. showSkybox controls only visibility, drawing the map or that sky whatever lights the scene. Clearing one asset keeps the other, and the SDK's environmentClearPatch() builds that edit. Older builds hold one environment asset at a time, so clear the other when setting one there
Three more optional fields shape the panorama through scene.patch: skyboxBlur (0 – 1, default 0) and skyboxIntensity (0 – 2, default 1) change only the visible skybox, and iblRotation (radians, default 0, free to wrap past a full turn) turns the panorama and its lighting together, including the sky that model-generated lighting uses. Older builds omit them, so offer each control only when its field is present
Physics
SceneEnvironment.physics is the scene's Physics switch: 3D objects fall onto the stage floor and land on VRM avatars. It defaults to false, and older builds omit it. The simulated poses are never saved, so object placements read back as written, and switching physics off puts every object back
A build with the spawn capability also accepts stage.spawn, which drops physics-driven copies of a registered prop asset onto the stage. The copies exist only in the renderer — no scene item, no layer row, no undo, nothing saved — and every copy of one file shares its geometry, textures and draw call, so counts in the hundreds stay cheap. The call answers invalid-state while the active scene's physics is off and not-found for an asset that is not a registered prop; otherwise it returns { spawned, alive }, where alive counts the bodies of every asset
| Field | Meaning |
|---|---|
assetId | A registered prop asset |
count | 1 – 200, default 1. Past 500 bodies on stage, the oldest make room |
origin | Centre of the scatter cube, { x, y, z } in metres. Default { x: 0, y: 3, z: 0 }, above where an avatar stands |
spread | Edge of the scatter cube, 0 – 20 m, default 1 |
velocity | The initial velocity every body shares, { x, y, z } in metres per second, each axis within ±50. Default at rest |
scale | 0.01 – 100, default 1 |
ttlMs | How long each body lasts, 0 – 600000 ms, default 30000. 0 keeps it until cleared |
transition | { style, inMs, outMs }: a Show and Hide style and separate entrance and exit lengths of 0 – 5000 ms, for these bodies alone. Omitted fields take the scene's |
Out-of-range numbers heal to their bounds. Bodies leave when their ttlMs runs out, when another scene is activated, when physics is switched off, or on stage.clearSpawned {}, which answers { cleared } and also cancels spawns still loading their asset
Scene Transitions
scene.activate waits for the destination scene to load and take over — the outgoing scene stays live and editable meanwhile — but not for its transition to finish revealing it. A newer activation supersedes a pending one, so the response reports whichever scene is actually active — not necessarily the one you asked for. The SDK allows activation 120 seconds unless you set requestTimeoutMs
A build with the scene-transitions capability carries each scene's transition — the effect played on entering it — in Scene.transition. scene.patch { transition } replaces it whole, without playing it:
| Field | Meaning |
|---|---|
type | cut, fade, wipe, circle, image or video |
durationMs | 100 – 10000 ms |
color | Hex colour of the cover that fades, wipes or circles in |
assetId | A registered image or video, or null |
switchPoint | Where in a video the scene switches, 0.05 – 0.95. Pick a frame that covers the whole stage |
autoFade | Fades a video in and out. Off by default |
fadeInMs / fadeOutMs | 0 – 10000 ms of playback each, default 300, capped at the time before and after the switch point. 0 turns that fade off |
Every way of activating a scene uses the destination's transition. Anything but a cut starts at once and holds its cover until the destination is ready, running past durationMs when loading is slow. A video plays muted with its speed fitted to the duration, and waits on its switch frame during that hold
Show and Hide
SceneEnvironment.itemTransition { style, durationMs } is the scene's Show and Hide setting: how its models, objects and spawned bodies appear and disappear. Older builds omit it. style is glitch (a dithered dissolve with tears and burns, the default), dither (the same dissolve with no extra effects), pop (scaling in and out) or cut, and durationMs runs 0 – 5000, default 300. A cut never ramps, whatever durationMs holds, and 0 behaves as a cut. Edit it through scene.patch with the environment
Layer Effects
A build with the layer-effects capability lets a single model or object carry its own effects, applied before it joins the scene. Every scene item has its own effects and effectLayers, shaped like the scene's, and instance.setEffects { instanceId, effects, effectLayers? } edits them:
effectstakes partial settings forcolor,levels,colorWheels,colorShift,selectColors,gradient,blur,bloom,diffusion,rim,outlineanddropShadow. Whatever you leave out keeps its valueeffectLayers, when present, replaces the list of added effects. An enabled effect is always listed
The response is the healed result: values clamped to their ranges, unknown keys dropped. A malformed effect fails the whole call before anything changes
Layer effects apply to models and to objects in 3D space. A 2D object answers unsupported-for-format; its stored settings are kept for when the space changes
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, pose and hand channels (faceSourceId, poseSourceId and handSourceId) 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
instance.setTrackingSources also takes handTrackingMode: arms lets tracked hands move a VRM's arms and wrists as well as its fingers, while fingers moves only the fingers, so a VMC or mocopi body keeps the arms
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
mediapipe is the webcam source. Only one can exist, and it has no port or phoneIp. Its mediapipe options switch its three tasks — face, hands and body, one per channel — and set deviceId ('' for the default camera), mirror and delegate (CPU or GPU). By default it is mirrored, runs on the CPU, and tracks face and hands but not body. tracking.addSource and tracking.updateSource accept a partial object and keep the options you leave out. Tracked hands also drive the hand inputs listed under Injection
tracking.status returns the aggregate state of the network face and pose sources, plus a sources map with every configured source's own status, the webcam's included; the tracking.status event carries the same shape and also fires when only one source's status changes
The legacy single-source methods still address the first network 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 master switches for network face and pose sources. The webcam answers only to its own enabled, set through tracking.updateSource, which keeps its task choices and avatar assignments
Microphone Lip Sync
Lip sync drives mouths from a microphone on the desktop. The microphone and the list of inputs are the desktop's, not those of the browser's computer, and neither the audio nor any calibration data crosses the API. The analysis also drives the voice inputs listed under Injection
| Method | Effect |
|---|---|
lipSync.state | Read the config, the capture status (off, starting, listening or error), the desktop's inputs, the live volume and vowel sample, and any calibration in progress |
lipSync.configure | Merge a partial config — enabled, deviceId, gain (0 – 30 dB), noiseGate (−60 – 0 dBFS), smoothing (0 – 0.3 s) — and return what was saved |
lipSync.restart | Restart capture on the next frame. It neither turns a disabled microphone on nor waits for capture, so poll lipSync.state |
lipSync.calibrate | Run one calibration step and return the new state |
The same config appears as settings.lipSync, and settings.patch accepts it too. Calibration uses the desktop's selected microphone and is shared with its panel: start opens a draft, record with a phoneme of A, I, U, E, O or S (background noise) records that sound for two seconds, preview applies the draft once all six are in, and save or cancel ends it. reset returns the current microphone to the bundled profile. Recording returns at once, so poll lipSync.state for progress: its calibration reports the phase (ready, recording or verifying), the sound being recorded and the ones completed. A step that does not fit the current phase answers invalid-state
instance.setLipSync { instanceId?, mode } sets whether the microphone drives a model: always (the default), when-untracked — only while its face is not being tracked — or off. Objects answer unsupported-for-format
Speech
speech.play plays audio and drives the mouth from its loudness. It works on instances advertising the speech capability — today a loaded Live2D model. Gate on the instance's runtime capabilities rather than the format: 0.53 and earlier hosts omit it under the legacy WebGL engine, which has since been removed
| 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
Automations
A build with the automations capability can list and run the application's automations — sequences of stage changes that start from a shortcut, an event or on request
| Method | Effect |
|---|---|
automation.list | List every automation |
automation.run | Run an automation by its ID |
Action payloads and trigger settings can only be edited in the desktop app. The list returns id, title, accelerator, registered, enabled, and the actionKinds and triggerKinds arrays — enough to display buttons without needing the full action configuration. When title is null, derive a label from actionKinds; the SDK's automationLabel does exactly that. actionSceneIds lines up with actionKinds and holds the destination of each switch-scene (null for every other kind), so a label can name the scene — which may have been deleted since
Every automation is a timeline whose timing, like the rest of its payload, stays in the desktop app, so the order of actionKinds says nothing about when an action runs. Action kinds include switch-scene and load-model, which run as setup before the timeline starts; effect-clip, a temporary effect; play-camera-motion and stop-camera-motion; play-audio and audio-control; and the avatar actions toggle-expression, play-motion, remove-all-expressions, load-model and model-position. delay is gone — a gap in the timeline does its job. Audio has no methods of its own: its files, playback and output device stay on the desktop, and a client plays sound by running an automation. triggerKinds lists the kinds of event trigger an automation has besides its shortcut — scene, model-loaded, motion, face-tracking, microphone and parameter today
accelerator is null when no keyboard or controller shortcut is assigned — automation.run still runs it. A paused automation has enabled: false and ignores every trigger, automation.run included. automation.run answers as soon as the run is dispatched, without waiting for its actions to finish. Whenever the list, its names, trigger kinds, enabled state or OS registrations change, automation.state delivers the whole list again
actionKinds and triggerKinds are 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 — automationActionLabel uses a generic label for unknown actions
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
The current protocol is 4. Every bump so far removed something from the wire, so a client or plugin written for an older protocol needs updating:
| Protocol | Change |
|---|---|
| 2 | Scene.behavior (Look at Cursor) is gone, from scene data and from scene.patch |
| 3 | stage.shockwave is gone; calling it answers unknown-method |
| 4 | shortcut.list, shortcut.trigger and shortcut.state became automation.list, automation.run and automation.state; the shortcuts capability became automations, and payloads use automationId and automations |
Last updated on September 20, 2026