00:00 / 00:00

Persona

Plugin API

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

ControlEffect
Enable APIStarts the server. Nothing connects without a key
PortDefault 25034
Allow Access on Local NetworkBinds 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

ActionEffect
Copy TokenCopy the full token to the clipboard
Rename KeyRename it in place
Revoke KeyDelete 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

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

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

EventFires when
scene.changedAny scene mutation: create, delete, rename, activate, or an edit
scene.loadingA scene apply or pipeline warm-up starts and finishes
settings.changedApplication settings changed
instance.loadedA model instance finished loading on stage
selection.changedThe editing selection moved, from any source
expression.changedThe active expression set changed
expression.persistenceA model's Remember Expressions flag was toggled
motion.startedA motion began playing; started carries oneShot on hosts with motion-stop
motion.endedA motion finished
speech.startedA speech.play utterance began
speech.endedIt finished, was stopped, or was superseded
hotkey.stateThe hotkey configuration changed
automation.stateThe automation list, names, trigger kinds, enabled state or OS registrations changed
tracking.statusFace or pose tracking status changed, or any one source's status — webcam face, hand and body tracking included
registry.changedA 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

GroupMethods
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:

FieldMeaning
idleAnimationThe idle master switch. false stops the VRM clip, and on Live2D every idle motion, the model's own Idle group included
idleClipVRM only. The idle clip's reference
idleMotionLive2D 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
trackingLostBehaviorNeeds 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
trackingLostMotionNeeds tracking-lost, Live2D only. The motion that replaces the idle once the face has been gone long enough; null keeps the idle
trackingLostDelayNeeds 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 fieldValuesDefault
density0 – 101
intensity0 – 101
range1 – 100 scene units from the camera20
qualitylow, medium, highmedium
speed0 – 5; 0 stops the haze moving0.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

FieldMeaning
assetIdA registered prop asset
count1 – 200, default 1. Past 500 bodies on stage, the oldest make room
originCentre of the scatter cube, { x, y, z } in metres. Default { x: 0, y: 3, z: 0 }, above where an avatar stands
spreadEdge of the scatter cube, 0 – 20 m, default 1
velocityThe initial velocity every body shares, { x, y, z } in metres per second, each axis within ±50. Default at rest
scale0.01 – 100, default 1
ttlMsHow 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:

FieldMeaning
typecut, fade, wipe, circle, image or video
durationMs100 – 10000 ms
colorHex colour of the cover that fades, wipes or circles in
assetIdA registered image or video, or null
switchPointWhere in a video the scene switches, 0.05 – 0.95. Pick a frame that covers the whole stage
autoFadeFades a video in and out. Off by default
fadeInMs / fadeOutMs0 – 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:

  • effects takes partial settings for color, levels, colorWheels, colorShift, selectColors, gradient, blur, bloom, diffusion, rim, outline and dropShadow. Whatever you leave out keeps its value
  • effectLayers, 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

MethodEffect
lipSync.stateRead 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.configureMerge a partial config — enabled, deviceId, gain (0 – 30 dB), noiseGate (−60 – 0 dBFS), smoothing (0 – 0.3 s) — and return what was saved
lipSync.restartRestart capture on the next frame. It neither turns a disabled microphone on nor waits for capture, so poll lipSync.state
lipSync.calibrateRun 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

ParameterMeaning
urlhttps:, http: (a local TTS bridge), or an inline data:audio/* payload
volumeOptional, 0..1
instanceIdOptional, 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

MethodEffect
automation.listList every automation
automation.runRun 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

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

MethodEffect
storage.getRead a key. An unset key returns value: null
storage.setWrite a key
storage.deleteDelete a key. An absent key still succeeds
storage.listList 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:

CodeMeaning
parse-errorThe frame is not valid JSON
invalid-requestThe envelope was malformed
unknown-methodThis build has no such method
invalid-paramsThe parameters failed validation
not-foundA referenced id resolves to nothing
unsupported-for-formatThe instance's format cannot do this — MToon on a Live2D model, say
conflictAnother session holds the parameter lease
renderer-unavailableThe stage could not answer — no window, or a reloading renderer
forbidden-pathThe path is outside every configured plugin folder
invalid-stateThe operation cannot complete: deleting the last scene, exceeding a storage quota, or failing to start audio
internalEverything 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:

CodeConstantCause
4001CLOSE_KEY_REVOKEDThe key was revoked
4002CLOSE_FORCE_DISCONNECTEDThe 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:

ProtocolChange
2Scene.behavior (Look at Cursor) is gone, from scene data and from scene.patch
3stage.shockwave is gone; calling it answers unknown-method
4shortcut.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

Tech otakus destroy the world