Persona runs an embedded local WebSocket server that allows external applications to inspect and drive the application: switch scenes, add and arrange layers, toggle expressions, trigger motions, stream lip-synced audio, monitor tracking status, and inject raw model parameters directly.
The server is disabled by default. Enable it under Settings → Plugin API, create an API key, and configure the generated token in your plugin or integration client.
Server Configuration
| Setting | Description |
|---|---|
| Enable API | Starts the WebSocket server. Requires a valid API key to authenticate incoming connections |
| Port | Default port is 25034 |
| Allow Access on Local Network | Binds beyond the loopback interface, allowing mobile devices or other computers on the LAN to connect |
When local network access is enabled, Persona displays its accessible local IP addresses and port, making it easy to configure connections from remote devices or the Web Console.
If the configured port is already occupied by another process, the settings panel displays an inline error status rather than failing silently.
API Keys
Authentication is mandatory: unauthenticated connection attempts are immediately rejected. Clicking Create Key prompts for a descriptive name and reveals the full token once upon generation; the copy button on the key row allows copying the token to the clipboard at any time.
| Action | Description |
|---|---|
| Copy Token | Copy the full authentication token to the clipboard |
| Rename Key | Update the descriptive name of the key |
| Revoke Key | Permanently delete the key. All active sessions using it are terminated and instructed not to reconnect |
Each key row displays real-time connection status: Connected, Never used, or a relative Last used … timestamp. When a client provides client information during handshake, the row records its reported name and version, keeping the key identifiable even when no client is currently connected.
Tokens are stored securely in the operating system credential vault (such as macOS Keychain or Windows Credential Manager). In environments where secure storage is unavailable, key generation fails with a descriptive error rather than writing plaintext credentials to disk.
Plugin Folders and Registration
Plugins can request Persona to register and load model or asset files. model.register and asset.register accept absolute paths only inside a user-configured plugin folder (configured via native dialog in settings); any other path is rejected with a forbidden-path error. Containment checks resolve symlinks on both sides.
With the asset-import capability, a client connected from the same computer (over loopback) can also call registry.pluginFolders (returning { folders }) to read the configured plugin folders, or asset.import { path, want? } to register a local file into the Inventory:
- Files inside a plugin folder or already in the Inventory register immediately without prompting; files elsewhere prompt the user in the desktop Control Panel (Add File, naming the client and API key label, and hiding the folder name while Stream Mode is on). If the user declines or does not respond within 45 seconds (
ASSET_IMPORT_PROMPT_MS), the call returnsforbidden-path; a concurrent import while a prompt is open returnsinvalid-state. Non-loopback clients receiveforbidden-pathfor both methods. - Both
asset.registerandasset.importwait for the stage to refresh its asset list before returning, so an immediateobject.addcan load the asset.
Registered asset IDs are content-addressed (a sha256(path) prefix), so they stay the same across restarts and never expose raw filesystem paths over the network. On ModelRef and AssetRef, origin: 'bundled' covers both content shipped with the app and content downloaded on demand from the Persona catalog on the desktop; catalog items that have not been downloaded are omitted, and the API has no download method.
asset.register takes a want field that asserts the asset kind; a mismatch returns invalid-params:
image,video,prop,ibl,lutanimationandcameraMotion, which share the.vmdextension and are told apart by their file headersaudio(WAV, MP3, OGG, FLAC, M4A or AAC files) andlyrics(.lrcfiles): asset inventory types, notObjectContent, so registering them does not create a stage layer
A model reference carries its entry file's basename as entryFile. From model.list only, it also carries its .vtube.json basename as vtubeFile (null without one, and never a directory). The SDK's modelMatchesFile resolves a model-load hotkey's stored file against these names, so match hotkeys against model.list, not against scene item references.
Connected Clients
While the server is active, the settings panel lists all open client sessions, displaying their declared application name, version, developer identifier, the API key in use, and the remote client IP. Browser-based clients also display their page origin.
Clients that do not send an identification payload are listed as Unidentified client and retain full API access. Clicking the disconnect button on a session terminates the connection and directs the client not to reconnect.
Whenever a new connection is accepted, Persona posts a silent desktop notification detailing the client name, key, and origin IP. If operating system permissions prevent notifications (such as on unsigned developer builds or disabled notification privileges), a toast appears in the Control Panel instead while it is open. Notifications never render onto the stage canvas, preventing them from appearing in video captures.
Treat API tokens as sensitive credentials. Holding a token grants
comprehensive control over Persona, including loading any model or asset
registered within configured plugin folders. Unless local network access is
explicitly enabled, the server binds strictly to the loopback interface
(127.0.0.1).
The Web Console
See Web Console for these controls.
Stream Deck
The official Stream Deck Plugin is another first-party client built on the Plugin API, mapping model and scene switching, hotkey triggers, expression and motion playback, and avatar transformation adjustments directly to physical hardware keys.
MCP Server
@laplace.live/persona-mcp is the official Model Context Protocol server, exposing the Plugin API to AI assistants (such as Claude, Cursor, and Codex) through task-oriented tool definitions alongside generic call_api RPC execution and stage.capture visual feedback.
SDK
@laplace.live/persona-sdk is the official TypeScript/JavaScript client library for Node.js, Bun, modern 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));All methods are governed by a shared, strongly-typed wire schema, providing compile-time parameter and return type inference. The SDK Reference details installation, client options, authentication, parameter lease management, and all exported types.
Events
Clients receive only the events they subscribe to. Following any reconnection, the SDK automatically restores all active subscriptions.
| Event | Description |
|---|---|
scene.changed | Scene lifecycle modification: creation, deletion, renaming, activation, or patch mutation |
scene.loading | Start or completion of scene activation or render pipeline shader warm-up |
settings.changed | Global application configuration updated |
instance.loaded | A model instance has completed loading and stage placement |
selection.changed | Active editor selection changed from any input source |
expression.changed | The active set of enabled facial expressions changed |
expression.persistence | The persistent expression toggle state changed on a model |
expression.groups | A model's expression groups changed, from any editor or a renamed or deleted expression file (hosts with expression-groups) |
motion.started | A motion clip started playback; includes the oneShot flag on hosts with motion-stop |
motion.ended | A motion clip finished playback |
speech.started | Synthesized audio initiated via speech.play started playback |
speech.ended | Speech playback finished, was stopped manually, or was superseded |
hotkey.state | An on-stage model's hotkey configuration or background-firing state updated; empty list when the model leaves the stage |
automation.state | Automation list, names, trigger kinds, enabled states, or background-firing states updated |
tracking.status | Aggregate face or pose tracking status, or any single source's state, updated (face, hand, and body tracking alike) |
registry.changed | Local model or asset registry updated, a file went missing or returned, or a model's thumbnail changed; refetch lists for that kind and drop cached registry.thumbnail results |
model.movement | A model's Movement Config changed from any editor (hosts with model-movement) |
model.versions | A model's version history changed (a version saved, named or switched to, history turned on or off), or Persona wrote one of its files; re-run model.listVersions (hosts with model-history) |
Method Overview
Methods scoped to a specific model accept an optional instanceId parameter, defaulting to the Primary Model of the active scene when omitted.
| Namespace | Methods |
|---|---|
scene.* | list get activate create duplicate rename delete patch inspect setLightFollowCamera |
preset.* | create update rename setTransition delete apply (requires the scene-presets capability) |
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 setText setSpace setPlacement attach setLightOverrides anchors |
expression.* | list active toggle getPersistence setPersistence, plus getGroups setGroups (requires the expression-groups capability) |
motion.* | list play playing stop |
speech.* | play stop |
hotkey.* | list set trigger |
automation.* | list run, plus get create update delete (requires automation-editing) and control status (requires automation-transport) |
model.* / asset.* | Respective list and register methods, asset.import and registry.pluginFolders (requires asset-import), registry.thumbnail, model.getMovement / model.setMovement for Live2D models, and model.listVersions setVersionHistory saveVersion nameVersion switchVersion restoreFiles compareVersions for Version History |
fonts.* | list (requires object-text capability) |
settings.* | get patch |
tracking.* / pose.* | tracking.addSource tracking.updateSource tracking.removeSource, setEnabled, setSource, 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 immediately, just as creating a scene in the desktop app does. The response's activeSceneId is the new scene's ID: use it directly instead of comparing scene lists, which can race with other client sessions.
scene.patch updates the active scene's background, camera, lighting, environment and Scene Transitions. Scene items change separately, through instance.* and object.*.
Setting a light's enabled to false turns off its light and shadows but keeps its other settings. The field defaults to true; older host builds omit it and can't turn lights off individually.
Camera
vrmCamera sets the camera in one batch: orbit, fov (1 – 179°), roll (rotation around the view axis, in radians), projection (perspective or orthographic) and clipAssetId.
- Orbit angles take any radian value, and
distancecan be negative. Mouse navigation keeps its narrower limits. - In orthographic projection, the view width is
abs(distance);fovis kept but has no effect. A negative distance flips both viewport axes. stage.resetCameraresets roll and projection to their defaults.- Older hosts don't report
rollandprojection, which then default to 0 andperspective. Check that the fields exist before showing their controls.
vrmCamera.follow makes the camera follow a layer: { instanceId, anchor, binding, damping }. It defaults to null and requires the camera-follow capability.
| Field | Description |
|---|---|
instanceId | The VRM model or 3D prop to follow. Pass { target: 'primary' } to follow whichever avatar is primary; following pauses while the primary is not a VRM |
anchor | On a model, { kind: 'bone', bone } (default head) or { kind: 'root' }. A 3D object always follows its root |
binding | How the target's rotation turns the camera: world ignores it; yaw, yaw-pitch and full follow yaw, yaw and pitch, or all three axes; lazy turns only when the anchor leaves the frame sideways |
damping | Smoothing time per axis in seconds, 0 – 10 (0 locks instantly): x, y, z for position and yaw, pitch, roll for rotation |
orbitsets the framing for when the target stands at the origin. While following, the camera moves with placement, walking, motions and tracking without writing back toorbit.- Picking another layer or anchor glides across. Removing the followed layer, or moving it into 2D, resets
followtonull. - A playing camera motion overrides following.
stage.resetCamerareframes the target and keepsfollow.
vrmCamera.aim turns the camera in place to keep a target in a chosen area of the screen: { instanceId, anchor, screenX, screenY, deadZone, softZone, bias, damping, lookahead }. It defaults to null and requires the camera-aim capability.
| Field | Description |
|---|---|
screenX, screenY | Where to hold the target, as a normalized screen position (0 – 1 from the top-left) |
deadZone | Width and height, each 0 – 1. While the target stays inside, the camera holds still |
softZone | Never narrower than the dead zone. Past it, the camera turns at full speed |
bias | Shifts the soft zone |
damping | Past the dead zone, the camera pans toward the target over damping.horizontal / vertical seconds |
lookahead | lookahead.time (0 – 1 s, 0 for off) anticipates target motion from its velocity, smoothed over lookahead.smoothing (0 – 30), with an ignoreY toggle |
- The camera's position and distance stay put; only the view direction turns, heading about world Y and tilting about the camera's local X axis while keeping lens roll.
- The camera holds still while gizmos are dragged or models load.
- Aim clears in the same cases as
follow, and camera motions override it.
vrmCamera.focus keeps the Depth of Field focus plane on a target layer and anchor instead of the look-at distance. It defaults to null and requires the camera-focus capability; its damping defaults to 0.2 s.
- If the anchor moves behind the camera, focus falls back to the look-at distance.
- Focus stays active during camera motion playback, and has no visible effect while Depth of Field is off.
vrmCamera.handheld sways the camera with procedural noise, for a natural handheld feel. It defaults to { enabled: false, intensity: 0.5, speed: 1 } and requires the camera-handheld capability; intensity ranges 0 – 1 and speed 0 – 2.
- Turning it on or off, or changing the intensity, eases over about 1 second.
- It is not saved to scene files, camera motion playback overrides it, and it pauses while viewport gizmos are dragged.
Objects and Text
object.add accepts place2d or place3d to place the prop in the same transaction that creates it, so it never renders a frame at the world origin. The values are clamped as in object.setPlacement.
object.addMany { objects } adds up to 100 objects in one scene edit, with a single save and a single undo step, and returns their instanceIds in request order. If any asset fails to resolve, the whole operation is rolled back.
With the object-text capability, ObjectContent can be a text layer: { kind: 'text', text, font, size, color, shear, letterSpacing, wordSpacing, lineHeight, align, verticalAlign, boxWidth, boxHeight, overflow, stroke, shadow, plate, sung, extrude, bevel, bevelResolution, roughness, metalness, emission, faceCamera }. One style applies to the whole layer.
| Field | Description |
|---|---|
font | A system face named by postscriptName, falling back to family, weight, italic and stretch where that face is missing. null uses the default sans-serif font |
size | In px. In 3D, text scales at 1000 px per meter, matching image quads |
boxWidth, boxHeight | In px |
letterSpacing, wordSpacing | In em |
lineHeight | A multiple |
align, verticalAlign | The placement pins the alignment anchor, like a Blender text's origin: left-aligned text grows rightward |
stroke, shadow, plate | 2D only: outline, shadow and backing plate. Their paint extents are in px |
sung: { color, strokeColor } | Paints what a lyrics clip has sung: the fill in both spaces, and the outline in 2D |
extrude, bevel | 3D only, in em |
bevelResolution | 3D only. Bevel segments |
roughness, metalness, emission | 3D only. How the glyphs are lit |
faceCamera | 3D only. Turns the text toward the view |
- In 3D, text renders as Blender-style extruded glyphs. Each space keeps the other space's fields untouched.
- Every field applies live.
object.setContenton a text layer edits it in place like a drag, with one undo step per typing pause. fonts.listreturns the desktop host's installed font families and faces, for remote font pickers.- Older hosts without this capability drop text layers.
object.setText { instanceId, text, transient? } changes only a text layer's text. It applies live, with one undo step per typing pause.
transient: trueshows temporary text without writing toscenes.jsonor recording an undo step. The text stays untiltext: nullis sent, the saved text is modified, or the layer leaves the stage (it is removed or another scene is activated).scene.inspectreports it as the object'sliveText.- Calling it on a non-text object returns
unsupported-for-format. - Text longer than
TEXT_LENGTH_MAXis truncated.
With object-video-sound, object.setContent edits a video layer's loop, muted and volume in place like a drag, with one undo step per pause, instead of swapping the scene. The sound plays through the desktop host's configured audio output device. Older hosts swap the scene for each edit, producing separate undo steps, and play sound through the system default device.
With object-chroma-key, image, video and webpage layers accept chromaKey settings, { enabled, color, similarity, smoothness, spill }, which object.setContent edits in place with one undo step per pause. Older hosts drop this field during normalization.
instance.attach { instanceId, attach } attaches a Live2D instance to another Live2D model as an accessory item, matching object.attach. Passing null detaches the item in place.
| Field | Description |
|---|---|
attach.depth | { kind: 'front' }, { kind: 'behind' } or { kind: 'artMesh', id }. The last places the item directly above that ArtMesh of the parent, falling back to front if the ID is missing |
attach.split | { itemArtMesh, depth } splits the item at one of its own ArtMeshes and draws the part beneath it at a second depth |
object.anchorsreturns anorderfield with each ArtMesh's draw order at the time of the query, which a depth control uses to order its options.- A circular attachment returns
invalid-params, and calling it on a VRM instance returnsunsupported-for-format. - With
object-pin-depth, a 2D image, video or webpage pinned to a Live2D model also respectsattach.depth, interleaving inside the parent model's stack. It renders whole, withoutsplitsupport. For 3D props, onlyfrontis supported for now. object-pin-depthalso stacks 2D webpages in scene order with post-processing effects, dropping the legacylayerfield. Older hosts ignore the depth setting, stack pinned media by scene order, and composite 2D webpages wholly behind or in front of the stage usinglayer.
Stage
stage.resetTransform { instanceId? } resets a layer's placement. Without instanceId, it resets the layer selected in the app, or the primary model if nothing is selected. A loaded avatar eases home over about ⅓ second.
stage.capture returns a transparent PNG screenshot of the stage viewport as { dataUrl, width, height }. It requires the stage-capture capability. The longest edge is scaled down to at most maxEdge (64 – 2048, default 1024), never up.
- Headless clients, such as AI agents using the MCP Server, rely on it to inspect what their scene changes did.
- Capturing stalls the rendering pipeline for one frame, so call it on demand rather than polling.
renderer-unavailablemeans the stage window isn't initialized or has no frame buffer to copy. While the stage recovers from a crash, retry until the first frame renders.
Capabilities
Application-level and instance-level capability flags declare which features the running host build supports. Always inspect capabilities rather than parsing semantic version numbers.
Application capabilities are reported in the hello handshake response and in app.info:
Names lead with what they extend (light-spot, object-text). Hosts also list each renamed capability's old name for older clients, and the SDK client folds old names into current ones.
Instance-level capabilities are returned in instance.list under capabilities; calling a method that an instance's format doesn't support returns unsupported-for-format. instance.list and instance.get also include loadError?: string | null explaining why the last load failed (null while loaded or still loading; omitted by older hosts).
Controllers
controllers exposes controller.* to read connected devices and saved profiles, rename profiles, set dead zones, remove profiles and start assignment. It also lets settings.patch set the controller.enabled master switch. These are controllers connected directly to the desktop host.
instance.setControllerMovement updates an instance's Controller Movement settings: enabled, slot (the saved profile number), moveSpeed, turnSpeed and smoothing. VRM models also accept walkEnabled, walkClip, walkSpeed and faceMovement; walkClip uses the same reference format as instance.setIdle. Out-of-range values are clamped, and calling the method on a non-avatar object returns unsupported-for-format.
Parameter Bindings
model-editing exposes instance.setBreath and binding.*, which reads, previews, writes and releases parameter bindings.
binding.overridesets temporary input overrides.binding.inputsreturns{ inputs, outputs }:inputsholds the raw tracking streams, andoutputsthe computed parameter readings after motions, expressions and physics. Outputs the model doesn't declare are left out.Time24handTime12hare built-in binding inputs with a 0 – 1 range. They sample the desktop's local wall clock once per rendered frame, independently of face tracking, for both Live2D and VRM bindings (see Time Parameter Bindings).TIME_INPUT_NAMES,TIME_INPUT_RANGESandisTimeInputNameexpose them in the SDK.
binding-calibration adds per-input calibration:
binding.getreturns it:{}when uncalibrated, and omitted by older hosts without the capability.binding.previewandbinding.setacceptconnections,calibration, or both. Whichever side is omitted stays untouched both live and on disk.- Calibrating alone never writes
bindingsto<model>.persona.json, so.vtube.jsonbindings stay active.
binding-random-pulses adds the built-in random pulse inputs RandomPulse1 through RandomPulse8 (0..1, independent of face tracking) and per-model randomPulses schedules:
- A binding whose inputs are all random pulses fires over whatever else moves its parameter (toward
outRange[1], leaving it alone between firings unless its rest output differs from the parameter's default), so it can share that parameter with another binding (see Parameter Bindings). binding.getreturnsrandomPulses:{}when no pulse has a custom schedule, and omitted by older hosts without the capability. Each schedule is{ intervalSeconds: [min, max], durationSeconds: [min, max], fadeInSeconds, fadeOutSeconds }(in seconds,0.1–3600for ranges and0–3600for fades); unconfigured pulses useDEFAULT_RANDOM_PULSE_SCHEDULE(0.5–4.5s interval,0.58s duration,0.29s fade in and out, matching VTube Studio's auto-blink).binding.previewandbinding.setacceptconnections,calibration,randomPulses, or any combination. Whichever parts are omitted stay untouched both live and on disk, and savingrandomPulsesalone does not copy.vtube.jsonbindingsinto<model>.persona.json.
Whole-Model Movement
With model-movement, model.getMovement and model.setMovement read and edit a Live2D model's Whole-Model Movement settings, as defined by VTube Studio.
| Method | Description |
|---|---|
model.getMovement { modelId? } | Returns { movement } |
model.setMovement { modelId?, movement } | Merges a partial update { enabled, x, y, z }, clamping amounts to integers within ±50 (MODEL_MOVEMENT_MAX). Returns { movement, saved }, where saved tells whether the settings were written to disk. It is false for read-only or unwritable files: the change then stays in effect until Persona quits, and the file's next successful write saves it |
modelIddefaults to the active primary model. An unknown model returnsnot-found, and a VRM model returnsunsupported-for-format.- The settings are stored per model, not per instance: in the model's
.vtube.jsonwhen present, otherwise in.persona.json, so they stay interchangeable with VTube Studio. - Movement shifts the model's placement directly, without writing parameter values, and stacks with any
FacePosition*parameter bindings. - Defaults match VTube Studio (enabled, 6/8/11). Every change fires the
model.movement { modelId, movement }event.
model-movement-pivot adds pivot to movement: the point the model grows around as z changes. It is { kind: 'root' }, the model's center and the default, or a point on an ArtMesh, { kind: 'artMesh', id, verts, weights }.
- The pivot is stored in the model's
.persona.json, even when the model has a.vtube.json. - It is evaluated at the point's position in the model's rest pose, so head motion never moves the model.
Expression Groups
With expression-groups, expression.getGroups and expression.setGroups read and edit a model's expression groups, following VTube Studio's Expression Groups rules: within an enabled group, showing one member hides every other shown member so the group shows at most one expression at a time.
| Method | Description |
|---|---|
expression.getGroups { modelId? } | Returns { supported, groups, edited }, each group { id, name, enabled, hideFirst?, entries: [{ expression, fadeSeconds, inverted }] }. supported is false when the model has no config file Persona can write; edited is true when .persona.json holds its own group list on a model that also has a .vtube.json (omitted by older hosts) |
expression.setGroups { modelId?, groups } | Replaces all groups with lenient validation: missing or duplicate id values are regenerated, entries without an expression are dropped, and fadeSeconds is clamped to 0 – 2 s. Returns the saved { supported, groups, edited } |
expressionis the member's key: its.exp3.jsonbasename on Live2D, its name on VRM, matched case-insensitively.- An expression counts as shown when it switches on, or when an
invertedmember switches off (an inverted member counts as shown while off, such as a "Hat Off" expression on a model that wears a hat by default). When a member is shown, every other shown member in each enabled group it belongs to is hidden (normal members switch off and inverted members switch on), each fading over its ownfadeSeconds. Hiding one member never shows another, so a group can have no members shown; if a member belongs to multiple groups that conflict, the later group wins. - With
expression-groups-hide-first, a group can sethideFirst: true(Hide, Then Show): when a member becomes shown, its fade-in waits until every other member hidden by that group has completely faded out (including inverted members fading in). Whenfalseor omitted, members Crossfade as in VTube Studio. Group-triggered hides, clearing all expressions, restores, reconciliations, and setting a weight to zero do not wait; older hosts without this capability drophideFirst. - A member's
fadeSecondsoverrides the fade duration of whatever triggered the switch, including a hotkey'sfadeSeconds. Disabled groups and their fade durations are ignored. - Groups apply regardless of how an expression is switched:
expression.toggle, hotkeys, automations, clearing all expressions (after which each group reconciles and keeps its first shown member), and restoring remembered expressions.expression.changedreports the resulting active expressions. expression.setGroupsimmediately reconciles the expressions shown on stage when enabling or disabling a group, or when adding, removing, or inverting a member. Renaming a group, changing fade times, or changinghideFirstdoes not trigger reconciliation, matching VTube Studio's editor.- Until
.persona.jsonhas its own group list, Persona imports.vtube.json'sExpressionGroupsread-only (edited: false). Callingexpression.setGroups(or editing groups in Persona) saves the full list to.persona.json, which takes precedence over.vtube.jsonfrom then on (edited: truewhen.vtube.jsonis also present) and never writes to.vtube.json. Renaming or deleting an.exp3.jsonin the Expression Editor updates matching entries in.vtube.jsonin place without.persona.jsontaking over, as well as in.persona.jsonif present. - Every change fires
expression.groups { modelId, groups, edited }(editedis omitted by older hosts), including updates caused by renaming or deleting an expression file.
Version History
With model-history, clients can read and manage a model's saved versions, corresponding to the Versions section in a model's details on the desktop (see Version History). Every method accepts modelId (defaulting to the primary model) and returns invalid-state for bundled models. Versions are saved only when explicitly requested.
| Method | Description |
|---|---|
model.setVersionHistory { enabled } | Enables or disables version history and returns { enabled }. Enabling history should be a user decision; turning it on saves an initial version |
model.listVersions { limit? } | Returns { enabled, versions, more, unsavedFiles }: up to limit ModelVersion entries newest first (1 – MODEL_VERSIONS_LIST_MAX, default MODEL_VERSIONS_LIST_DEFAULT), whether older versions exist, and the file paths on disk that differ from the current version (ignoring which expressions are toggled on) |
model.saveVersion { name?, notes? } | Saves the current files as a version and returns { version } (or returns the current version if nothing changed and no name was provided) |
model.nameVersion { versionId, name, notes? } | Names or renames a version, or clears its name when name: null. Names are case-insensitively unique across versions; duplicate names return invalid-params |
model.switchVersion { versionId, unsavedChanges? } | Switches the model's files to the specified version while preserving active expressions, reloads the model, and returns { version, backup }. Switching does not create a new version |
model.restoreFiles { versionId, paths, unsavedChanges? } | Restores only the listed file paths and returns { backup }; restored files remain unsaved changes until the next save |
model.compareVersions { from, to } | Returns { files }: every path present on either side with its status (MODEL_VERSION_CHANGE_STATUSES). Either side may be MODEL_VERSION_CURRENT for the current files on disk; config files that differ only in active expressions are marked unchanged |
- A
ModelVersionhas{ id, parentId, time, kind, message, name, notes, current }.kindis one ofMODEL_VERSION_KINDS(first,saved,backup), ornullfor a commit created by another Git tool (in which casemessageis the first line of its commit message).currentmarks the version that the files on disk are based on. - When switching or restoring would overwrite files with unsaved changes, the call returns
invalid-stateunlessunsavedChanges(MODEL_VERSION_UNSAVED_ACTIONS) is specified:'save'first saves all unsaved changes as abackupversion (returned inbackup), while'discard'discards the overwritten changes. - While version history is disabled, saving, switching, and restoring return
invalid-state; listing, comparing, and naming can still read previously saved history. - If the model's files are missing, enabling history, saving, switching, and restoring return
not-found. Enabling history on a folder that version history cannot read (such as a UNC share, or a folder whose name contains a backslash on macOS or Linux) returnsinvalid-state. - Unwritable files, or a repository left in an unfinished state by another Git tool (staged files, an in-progress merge, or detached HEAD), return
invalid-statewithout modifying any files. - Names are up to 80 characters (
MODEL_VERSION_NAME_MAX_LENGTH) and notes up to 2,000 (MODEL_VERSION_NOTES_MAX_LENGTH). - Every change fires
model.versions { modelId }; file edits made by external applications do not fire this event. Older hosts do not support these methods.
Motions and Idle
motion.list retrieves motion groups configured on a model. For Live2D models, the host also indexes .motion3.json files in the model folder that .model3.json doesn't declare, creating a motion group named after each file. Motions recorded in the desktop Motions tab appear without a model reload.
motion.stop ends the running one-shot motion and eases the model back to idle rather than snapping (motion.ended fires at once, not after the ease). It requires the motion-stop capability. If only the idle animation is playing, the call does nothing and returns { stopped: false }.
motion.playing reports a oneShot flag, also sent in motion.started, that tells whether the clip is an overlay animation above the idle, the kind motion.stop ends. Older builds omit this field.
instance.setIdle updates idle configuration for an instance:
| Field | Description |
|---|---|
idleAnimation | Master toggle for idle animation. false stops the VRM animation clip, or all Live2D idle motions (including the model default Idle group) |
idleClip | VRM only. Reference to the assigned idle animation clip |
idleMotion | Live2D only, requiring idle-motions capability. Motion file to loop underneath all animations; null loops one motion picked at random from the built-in Idle group. A file that no longer exists falls back to null |
trackingLostBehavior | Requires idle-tracking-lost capability. hold freezes the last tracked pose; idle smoothly transitions to idle over about 0.3 seconds and returns when tracking recovers |
trackingLostMotion | Requires idle-tracking-lost capability, Live2D only. Motion played in place of idle when tracking is lost past the configured delay; null disables override |
trackingLostDelay | Requires idle-tracking-lost capability. Tracking loss timeout threshold in seconds, 0 – 60 |
Calling instance.get returns these idle settings alongside instance state.
Lighting
With the light-area capability, SceneLight accepts the area type: a rectangular light at { x, y, z }.
| Field | Description |
|---|---|
azimuth, elevation | Orientation, in degrees |
roll | In-plane rotation, −180 – 180°, default 0 |
width, height | Size, 0.01 – 20 units each, default 1 |
- Area lights illuminate PBR materials and MToon avatars; on MToon, the toon shader approximates them with four sample points. They don't affect Live2D or cast shadows.
rangeand shadow settings are preserved when the light changes type.
With the light-spot capability, SceneLight accepts the spot type: a spotlight at { x, y, z }.
| Field | Description |
|---|---|
azimuth, elevation | Beam direction. When both are zero, the beam points toward −Z; positive elevation tilts it downward |
range | 0.1 – 20 units |
angle | Half-angle of the cone, 1 – 89°, default 30 |
penumbra | Softens the cone edge, 0 – 1, default 0.3 |
- Intensity ranges 0 – 20, as for point lights.
- Spotlights illuminate PBR and MToon materials, support shadow quality and radius adjustments, and cast volumetric fog. Creating area lights and spotlights, or converting a light to either type, works only on hosts with the matching capability.
shadowFilter selects pcf (the default) or pcss filtering for each directional, point or spot light, and both can be mixed in one scene. Gate it on the light-shadow-filter capability: locally created lights can carry shadowFilter even on hosts that don't support it. Older scenes load with PCF.
| Field | Description |
|---|---|
shadowQuality | Shadow map resolution; off disables shadows |
shadowRadius | PCF edge softening, in shadow map texels. PCSS leaves the saved value untouched |
shadowSourceSize | Light size for PCSS, which searches for blockers and widens penumbrae with distance. Degrees for directional lights, meters for point and spot lights; 0 – 10, default 0.1. 0 gives hard shadows |
followCamera makes a light move with the camera. It defaults to false, requires the light-camera-follow capability, and ambient light ignores it. When true, the light's position and orientation are computed relative to the camera, so it moves with camera navigation and trajectory playback.
followCameraOptions chooses which parts of the camera transform to follow; each defaults to true:
| Field | Description |
|---|---|
position | Follows the look-at target and pan |
rotation | Follows world-space rotation, including roll |
distance | Follows the signed orbit distance |
- Modifying
fovalone does not move the light. - A disabled component freezes at its value stored in
followCameraReference:targetX,targetY,targetZ,distanceand a world-space rotation quaternion. Without a reference, it uses the origin, no rotation and zero distance.
scene.setLightFollowCamera { lightId, followCamera, options? } turns camera following on or off for one light in the active scene, or merges new options, without moving or rotating the light in world space. Like followCamera, it requires light-camera-follow.
- The desktop recalculates the light's coordinates from the displayed camera, including camera motion playback, and returns
{ light }, orlight: nullif the light was deleted. scene.patchand scene loading apply the values as written, without this conversion.
SceneLight.volumetric toggles illuminated volumetric fog per point, spot, or area light (default false); other light types ignore it. Older builds used a single global toggle; scenes saved under that setting automatically enable volumetric on all supported lights upon loading.
SceneEnvironment includes optional volumetricLighting and clusteredLighting settings. Older builds omit them, so check that a field exists before offering its control.
volumetricLighting defines scene-wide fog parameters. It has no switch of its own: fog renders only when at least one active light has volumetric illumination enabled. Current hosts return the complete settings; edit them through scene.patch:
| Property | Range | Default |
|---|---|---|
density | 0 – 10 | 1 |
intensity | 0 – 10 | 1 |
range | 1 – 100 scene units from camera | 20 |
quality | low, medium, high | medium |
speed | 0 – 5 (0 pauses fog motion) | 0.2 |
Fog scatters around enabled lights, obeys depth occlusion, and remains transparent where unlit. While any active light has fog enabled, the point, spot and area lights built into 3D props and environment models scatter it too.
Fog requires a perspective camera; orthographic views preserve settings but skip rendering. Directional and ambient lights do not participate, and Live2D does not occlude fog.
clusteredLighting: true accelerates forward lighting for 1 – 64 bounded, non-shadowed point lights under WebGPU perspective cameras. It defaults to false. A point light without a bounded range, more lights than that, or an unsupported renderer or camera projection falls back to standard shading. It doesn't reduce shadow rendering cost or change which lights the fog samples.
In builds with environment-map-model, SceneEnvironment can specify both iblAssetId and modelAssetId:
bakeFromModelcomputes an environment probe from the 3D model, using the environment map as input; whenfalse, the map illuminates the scene directly, falling back to the model's built-in sky if there is no map.showSkyboxcontrols only the background, which shows the map or the model's sky, whatever provides the lighting.- Clearing one asset preserves the other;
environmentClearPatch()generates that patch. Older builds allow only one environment asset at a time, so clear the other one when setting a new one.
scene.patch also provides these optional panoramic controls. They exist only on supporting builds, so check that a field exists before showing its control:
| Field | Description |
|---|---|
skyboxBlur | 0 – 1, default 0. Affects only the visible skybox background |
skyboxIntensity | 0 – 2, default 1. Affects only the visible skybox background |
iblRotation | In radians, default 0, and may go past a full turn. Rotates the map and its lighting environment together, including the sky used for model-generated lighting |
Physics Simulation
SceneEnvironment.physics controls the global Physics Simulation: dynamic 3D props fall under gravity and collide with the stage floor and VRM avatars. It defaults to false, and older builds omit it. Simulated transforms are transient and never persisted, so object positions read through the API are always the initial placements; disabling physics immediately returns all props to those placements.
stage.spawn drops registered prop assets onto the stage as dynamic physics bodies. It requires the stage-spawn capability and returns { spawned, alive } on success, where alive counts every physics body now on the stage.
Spawned bodies exist only in renderer memory: they create no scene items or layer entries, cannot be undone and are not saved. Copies of one asset share geometry, textures and draw calls, so hundreds of them stay fast. A call returns invalid-state while physics is disabled. An unknown asset returns not-found, and an asset that isn't a prop returns invalid-params.
| Field | Description |
|---|---|
assetId | Registered prop asset ID |
count | Quantity to spawn, 1 – 200, default 1. Viewport limit is 500 active bodies; past it, the oldest are removed first |
origin | Spawn center { x, y, z } in meters, default { x: 0, y: 3, z: 0 } (above avatar head) |
spread | Bounding spawn cube size in meters, 0 – 20, default 1 |
velocity | Initial linear velocity { x, y, z } shared by every body, in m/s, each axis within ±50, default zero |
scale | Uniform scale multiplier, 0.01 – 100, default 1 |
ttlMs | Lifespan in milliseconds, 0 – 600000, default 30000. 0 persists until cleared |
transition | { style, inMs, outMs }: custom Show and Hide animation, with entrance and exit times of 0 – 5000 ms each; omitted fields inherit from scene |
Out-of-bounds parameters clamp to valid ranges. Spawned items despawn upon ttlMs expiration, scene change, or disabling physics. Calling stage.clearSpawned {} immediately purges all active bodies, returns { cleared }, and cancels spawns whose assets are still loading.
Scene Transitions
scene.activate blocks until the target scene completes loading and assumes stage rendering, without waiting for the transition animation to uncover the viewport. While the target loads, the outgoing scene keeps running and stays editable. Subsequent switch requests preempt pending operations, and the response reports the final active scene ID. The SDK defaults to a 120-second timeout, overridden by requestTimeoutMs.
Builds supporting scene-transitions report each scene's Scene Transition, the effect played when entering it, under Scene.transition. scene.patch { transition } replaces the configuration without initiating playback:
| Field | Description |
|---|---|
type | cut, fade, wipe, circle, image, or video |
durationMs | Duration in milliseconds, 100 – 10000 |
color | Hexadecimal color for fade, wipe, or circle wipes |
assetId | Registered image or video asset ID, or null |
switchPoint | Video timestamp ratio (0.05 – 0.95) where scene cut occurs, chosen where video covers stage |
autoFade | Automatically fade video in and out, default off |
fadeInMs / fadeOutMs | Fade durations in milliseconds, 0 – 10000, default 300, capped at the time before and after the switch point respectively; 0 disables that fade |
All scene activations execute the transition defined on the target scene. Every transition except cut begins immediately and holds an opaque cover until the new scene is ready, extending beyond durationMs if assets load slowly. Video transitions play muted, rate-scaled to duration, pausing on switchPoint until the scene is prepared.
Scene Presets
Builds supporting scene-presets list the scene's saved scene presets under Scene.presets in display order, each as { id, name, transition, items }. Older hosts omit presets.
itemsis keyed by instance ID, and each value is{ visible, place }.placeis{ space: '2d', x, y, scale, rotation }for Live2D models and 2D objects, and{ space: '3d', x, y, z, rotX, rotY, rotZ, scale }for everything else. Opacity is not part of a preset.- Layers added after a preset was saved have no entry and are left as they are when it is applied.
preset.* methods all act on the active scene:
| Method | Request | Description |
|---|---|---|
preset.create | { name?, transition? } | Saves the current layout as a new preset appended to the list; the name defaults to the next in sequence in the app's UI language (e.g. "Preset 1"), the transition to glide |
preset.update | { presetId } | Re-saves the preset from the current layout |
preset.rename | { presetId, name } | Renames the preset |
preset.setTransition | { presetId, transition } | Changes how the stage moves into the preset |
preset.delete | { presetId } | Deletes the preset, leaving layers where they are |
preset.apply | { presetId } | Moves the stage to the preset's layout; returns as soon as the transition starts, without waiting for layers to settle |
create, update, rename, and setTransition return { preset }; delete and apply return {}.
transition sets how the stage moves into the preset, and visibility changes follow the same choice:
| Value | Description |
|---|---|
glide | Moved layers ease into place |
item | Every changed layer exits with the scene's itemTransition, then re-enters at its new place |
cut | Everything snaps into place |
- Saving mid-transition records where layers are about to settle. Calling
applyagain for the preset the stage is already heading to changes nothing. - A preset ID not found in the active scene returns
not-found. Preset IDs are unique only within a scene, and duplicating a scene keeps them. - On the desktop, preset edits and applies count as scene undo steps, and every change is pushed through
scene.changed. - Keyboards, controllers, and Stream Deck trigger presets through automations containing an Apply Preset action (or the Stream Deck Apply Preset key).
Background Opacity
Builds with the background-alpha capability let SceneBackground.color carry an alpha byte (#rrggbbaa), so a transparent stage is a color background at alpha 00 rather than a mode of its own (see Stage Background). Hosts still accept the old transparent mode (LEGACY_TRANSPARENT_BACKGROUND) and store it as that color at zero alpha.
Older hosts without the capability keep that mode and heal an alpha byte to their default color. The SDK client adapts to such a host:
- It reads the host's
transparentas an alpha-0 color. - It sends a
scene.patchcolorbackground at alpha00astransparent. - It drops the alpha byte from any other background color.
Show and Hide
SceneEnvironment.itemTransition { style, durationMs } configures Show and Hide: how models, props, and spawned items animate when they enter or exit the stage. Older builds omit it. style accepts:
| Value | Description |
|---|---|
glitch | Dithered dissolve with tears and burns (default) |
fade | Smooth whole-layer alpha dissolve, replacing the legacy dither |
pop | Scale bounce |
cut | Instant transition |
durationMs ranges 0 – 5000 ms, default 300 ms. With cut, or a durationMs of 0, layers appear and disappear instantly. Transitions are updated via scene.patch.
Layer Effects
Builds with layer-effects allow attaching dedicated Visual Effects to individual models or props before scene composition. Each scene item maintains independent effects and effectLayers definitions matching scene effect schemas, modified via instance.setEffects { instanceId, effects, effectLayers? }:
effectsaccepts partial configurations forcolor,levels,colorWheels,colorShift,selectColors,gradient,blur,bloom,diffusion,sketch,rim,outline, anddropShadow; omitted parameters keep their valueseffectLayerscompletely replaces the ordered stack of added effect layers; enabled effects are always in the list
The response returns the sanitized configuration: values clamp within valid ranges, and unknown keys are discarded. Malformed payloads reject the entire call atomically.
Bloom (bloom) lighting follows Warudo's Camera Bloom (Beautify specification):
| Field | Range | Default | Description |
|---|---|---|---|
intensity | 0 – 10 | 0.1 | A plain gain; 0 adds no glow |
threshold | 0 – 5 | 0.75 | A gamma-space per-channel cut, squared into linear light and capped at 1000 |
radius | 0 – 6 | 3 | Scaled with a 1080p frame height |
Rim Light (rim) adds light following VTube Studio's backlight behavior: white rims clip to white and fade naturally into the subject's local color. It has no blendMode (legacy values are dropped). The default color is #ffeb74 and the default size is 0.5.
Outline (outline) uses normalized size, color, opacity, quality (low, medium, high), and roundness, a normalized closing radius that fills narrow gaps and rounds corners (0 follows the silhouette).
Sketch is a pencil drawing on paper after Jaume Sanchez's NPR sketch shader. It is available as environment.effects.sketch and as a layer effect:
| Field | Range | Description |
|---|---|---|
paperColor | — | Paper color |
lines | 0 – 2 | Line strength |
lineWidth | 0.5 – 4 | Line width, in CSS pixels |
shading | 0 – 2 | Shading |
softness | 0 – 1 | Softness |
saturation | 0 – 1 | Color saturation |
opacity | 0 – 1 | Opacity |
- In scene scope,
paperBackground: truefills transparent pixels with paper. Layer sketches always preserve source alpha, regardless of this field. - Older hosts omit
sketchfrom their returned effects.
Fisheye is scene-only, at environment.effects.fisheye: the frame is redrawn as a fisheye lens sees it, bulging from the center. strength (0 – 1, default 0.5) sets the bend; 1 is a 150° fisheye across the frame diagonal. The corners hold still, so the frame stays filled and its edges crop. Older hosts omit fisheye from their returned effects.
Layer effects apply to avatar models and to props in 2D or 3D space. 2D images, videos, webpages, and text require the host's layer-effects-2d capability; their effects composite at the object's position in the layer stack (pinned or not) and fade with the object's opacity. Older hosts return unsupported-for-format for 2D props but preserve saved configurations. Saved configurations are likewise preserved when an object switches space.
Tracking Sources
Persona can run multiple tracking sources concurrently. Sources are listed in settings.tracking.sources and managed via tracking.addSource, tracking.updateSource, and tracking.removeSource.
instance.setTrackingSources maps an instance's face, pose, and hand channels (faceSourceId, poseSourceId, handSourceId) to configured sources. Setting a channel to null, or to an ID that doesn't exist, disables tracking for that channel. New instances bind to global default sources; a single source can drive multiple avatars simultaneously.
instance.setTrackingSources also configures handTrackingMode: arms drives VRM arms, wrists, and fingers from hand tracking; fingers articulates fingers only, preserving arm posing for VMC or mocopi body trackers.
Face and pose sources come in these kinds:
| Kind | Channel | Default port |
|---|---|---|
persona-ios | Face | 19700 |
ifacialmocap | Face | 49983 |
vts-ios | Face | Fixed by its protocol |
vmc | Pose | 39539 |
mocopi | Pose | 12351 |
- Face sources take an optional
phoneIpfilter. - A source added without a
porttakes the first free port from its kind's default. - Requests fail if ports collide, though multiple
ifacialmocapsources, or multiplepersona-iossources, can share a single socket. - The deprecated
vts-ios-nativeis rejected at the protocol layer.
mediapipe represents the Webcam Tracking engine. It runs as a singleton without port or IP settings, and takes these options:
| Field | Description |
|---|---|
face, hands, body | Toggle each tracking task; each feeds its own channel |
deviceId | The camera to use; an empty string selects the default camera |
mirror | Mirrors the image |
delegate | Compute backend, CPU or GPU |
By default, the image is mirrored, tracking runs on the CPU, face and hand tracking are on, and body tracking is off. tracking.addSource and tracking.updateSource accept partial options and leave the rest unchanged. Hand tracking also drives the hand inputs detailed in Parameter Injection.
tracking.status reports aggregated status for network face and pose trackers alongside each configured source's state, webcam included, under the sources map; the tracking.status event dispatches identical payloads whenever source states change.
Legacy single-source methods continue to operate on the first network source for the corresponding channel:
tracking.setSourcechanges its kind.pose.setSourceswitches its protocol, creating the source if there is none, and returns its port.pose.setPortupdates its listening port.tracking.setEnabledandpose.setEnabledremain global master toggles for network face and pose sources.
Webcam tracking follows only its own enabled setting, changed through tracking.updateSource. While it is off, its task toggles and avatar channel assignments are kept. Webcam inference requires the desktop Webcam Tracking Add-On; the API does not install add-ons, and until the add-on is installed on the desktop host, the mediapipe source's status remains waiting.
Microphone Lip-Sync
Lip-Sync drives avatar mouth shapes from the host microphone input. Device enumeration and audio capture run on the desktop host. Raw audio and calibration coefficients stay on the desktop: neither crosses the API or appears in public settings. Audio spectral analysis also drives audio inputs detailed in Parameter Injection.
| Method | Description |
|---|---|
lipSync.state | Inspect configuration, capture status (off, starting, listening, error), input devices, live volume and vowel samples, and active calibration progress |
lipSync.configure | Update partial configuration (enabled, deviceId, gain 0 – 30 dB, noiseGate −60 – 0 dBFS, smoothing 0 – 0.3 s) and return the complete saved configuration |
lipSync.restart | Reinitialize capture on the next frame. It doesn't turn on a disabled microphone or wait for capture to start; poll lipSync.state to verify readiness |
lipSync.calibrate | Advance calibration sequence and return updated progress |
Configuration is mirrored under settings.lipSync and mutable via settings.patch.
Calibration runs on the selected desktop microphone and stays in sync with the desktop Control Panel, in these steps:
| Step | Description |
|---|---|
start | Creates a draft |
record | Samples a specific phoneme for two seconds: A, I, U, E, O, or background noise S |
preview | Applies the candidate profiles once all six sounds are recorded |
save / cancel | Commits or discards the changes |
reset | Restores the default profile for the current microphone |
Recording returns immediately; poll lipSync.state to monitor progress. Its calibration reports the current phase (ready, recording, or verifying), the phoneme being recorded, and the phonemes already recorded. A step sent in the wrong phase returns invalid-state.
instance.setLipSync { instanceId?, mode } toggles microphone driving per model: always (default), when-untracked (active only when face tracking is lost), or off. Calling on non-avatar objects returns unsupported-for-format.
Speech Playback
speech.play plays an audio stream and animates avatar mouth shapes based on audio energy. It applies strictly to instances declaring the speech capability (currently loaded Live2D Models). Availability is governed dynamically by instance capabilities rather than file formats: 0.53 and earlier, for example, don't declare it under the legacy WebGL engine.
| Parameter | Description |
|---|---|
url | Audio resource URI: supports https:, http: (local TTS bridge), or inline data:audio/* payloads |
volume | Optional volume scalar, 0..1 |
instanceId | Target instance ID, defaulting to the primary model |
A model can play only one speech stream at a time; new invocations immediately preempt existing playback. Successful return confirms that playback has begun; failures return invalid-state. Lifecycle is bounded by speech.started and speech.ended events, and playback can be terminated early via speech.stop.
Avatar Hotkeys
Model-embedded Hotkeys are inspected and triggered via three methods, each accepting an optional modelId defaulting to the primary model:
| Method | Description |
|---|---|
hotkey.list | List all configured hotkeys on the model |
hotkey.set | Overwrite hotkey configurations; the server heals them leniently |
hotkey.trigger | Trigger a hotkey action by identifier |
Each stage model maintains its own active hotkey bindings regardless of whether it is primary, Live2D, or VRM. Pressing a physical key combination triggers matching bindings across all models simultaneously. Invoking hotkey.trigger on an unplaced model is silently ignored.
Hotkey objects support four optional properties derived from VTube Studio specifications:
| Field | Description |
|---|---|
hold | The expression stays active only while the key is held down |
fadeSeconds | Fade-in and fade-out time of the expression or motion, 0 – 2 s. null uses the file's own setting. An enabled expression group member's fade wins over it |
deactivateAfterSeconds | How long until the expression dismisses itself, or the longest a Live2D motion plays, 0.1 – 3600 s. null disables the timeout |
stopsOnLastFrame | A Live2D motion pauses on its final frame until triggered again |
Older hosts don't return these fields. Properties left out of hotkey.set keep their existing values, so older clients never wipe these settings.
Because hotkey.trigger is an instantaneous remote event without a release signal, hotkeys marked with hold operate as state toggles. Expressions toggled via hold or deactivateAfterSeconds are never persisted to disk.
Hotkey.accelerator is up to three +-joined tokens fired as a set: modifiers (MODIFIER_TOKENS: Control, Alt, Shift, Super), keys in Electron's names, the mouse buttons LeftMouseButton, RightMouseButton, and MiddleMouseButton, or one controller's Controller* buttons. Order doesn't affect matching, so compare combos with the SDK's comboKey, not as strings, and format them for display with acceleratorLabel(accelerator, app.platform) using the desktop's app.platform from hello.
HotkeyState.registered lists the hotkeys that currently fire while Persona is in the background: any global combo, including a single bare key, since the desktop reads key state directly rather than registering shortcuts with the OS. Older hosts list only the Control, Alt, or Super combos they registered with the OS.
Automations
Builds with automations support inspecting and executing Automations, which are multi-action sequences triggered by hotkeys, system events, or API calls; builds with automation-editing and automation-transport can also read and edit the full timeline definition and control timeline playback.
| Method | Description |
|---|---|
automation.list | Retrieve summary entries for all configured automations |
automation.run { automationId } | Execute an enabled automation workflow by identifier |
automation.get { automationId } | Return the full definition { automation } (AppAutomation, requires automation-editing) |
automation.create { automation } | Create an automation and return { automation } (requires automation-editing) |
automation.update { automationId, automation } | Replace an automation's timeline and triggers and return { automation } (requires automation-editing) |
automation.delete { automationId } | Delete an automation (requires automation-editing) |
automation.control { automationId, command, positionMs? } | Drive timeline transport (play, pause, stop, seek, prepare) and return { run } (requires automation-transport) |
automation.status { automationId } | Inspect the current run and step status { run } (requires automation-transport) |
automation.list returns lightweight summary metadata: id, title, accelerator, registered (whether it currently fires while Persona is in the background; on older hosts, whether it is registered with the OS), enabled, and the actionKinds and triggerKinds arrays, providing enough metadata to build control surfaces without fetching the full timeline.
When title is null, SDK utilities such as automationLabel build a descriptive label from actionKinds. actionSceneIds lines up with actionKinds to give the target scene ID of each scene-switch action (null for other kinds), so clients can show the target scene's name.
Each automation runs as a timeline, and the order of actionKinds does not imply execution order. Action kinds use category-first names:
| Category | Action Kinds |
|---|---|
| Setup before the timeline runs | scene-switch, model-load (matching AUTOMATION_SETUP_KINDS and isAutomationSetup) |
| Visual effects | effect-clip, effect-toggle, effect-params |
| Layer visibility and presets | layer-visibility, preset-apply (moves the active scene to one of its presets, skipped if missing) |
| Karaoke lyrics and text | lyrics-play (renders an .lrc asset on a text layer over its duration), text-set |
| Camera controls | camera-pose, camera-reset, camera-motion-play, camera-motion-stop, camera-follow, camera-aim, camera-focus, camera-handheld |
| Audio playback | audio-play, audio-control |
| Avatar and Stream Mode | expression-toggle, motion-play, expression-clear, model-load, model-position, stream-mode |
- The
delayaction has been removed; delays are represented by blank timeline intervals. - There is no separate audio API: audio files, playback and output devices remain local to the desktop, and clients run or play automations containing
audio-playto trigger sound effects. - Deprecated action names sent by older hosts (defined in
LEGACY_AUTOMATION_ACTION_KINDS, acrossactionKindsandHotkey.action) are normalized by the SDK client on receipt, or throughcanonicalActionKind. triggerKindslists the event triggers bound besides the shortcut, currentlyscene,model-loaded,motion,face-tracking,microphone, andparameter.
With automation-editing, automation.get returns the full AppAutomation (durationMs, autoDuration, actions with id/startMs/durationMs/sourceInMs, triggers, cooldownMs, accelerator, global, and enabled):
automation.createandautomation.updateaccept anAutomationDefinition(validated byAutomationDefinitionSchema, returninginvalid-paramswhen fields or timing are invalid).- Newly created automations start disabled (
enabled: false), with no shortcut (accelerator: null,global: false), and fresh server-assigned IDs so nothing fires until the user enables it. automation.updatereplaces the actions, triggers, duration, and cooldown and regenerates step IDs while preserving the saved shortcut (accelerator),globalflag, andenabledswitch.- Creating, updating, or deleting an automation via the API cannot be undone and clears the desktop timeline editor's undo history for that automation.
With automation-transport, automation.control and automation.status drive and inspect desktop timeline playback (AutomationRunStatus: { automationId, phase, positionMs, durationMs, error, steps }, or run: null before any run, preview, or prepare in the current session):
playplays the timeline to the end (even whenenabledisfalse, continuing from the current playhead when paused or scrubbed),pausepauses playback,preparepreloads media and animation assets,seekmoves the playhead topositionMsand previews Effect Clip, Camera Pose, Layer Visibility, Set Text, and Play Lyrics on stage, andstopstops playback and releases previewed state.- The session that last controlled an automation's transport owns it. If that client disconnects, a paused, previewing, or failed run is stopped so shortcuts and triggers work again; a playing or preparing run is stopped if it later pauses or fails.
Automations without a keyboard, mouse, or controller shortcut report accelerator: null and can be triggered freely via automation.run. Automations with enabled: false ignore all triggers, including automation.run (though automation.control with play still plays them). automation.run returns immediately upon dispatch, without waiting for the timeline to finish. Changes to automation lists, definitions, enabled states, or background-firing states broadcast an updated summary list via automation.state.
actionKinds and triggerKinds are open enumerations: newer host builds may
introduce new action types unsupported by older SDKs. Clients should handle
unrecognized types gracefully without throwing parse errors. The SDK
automationActionLabel utility maps unknown types to a generic fallback
label.
Settings
settings.get and settings.patch access and update a curated allowlist of global application settings (window pin, tray minimization, performance parameters, gamepad switches), rather than exposing the raw store.
Responses and settings.changed events include read-only window geometry:
window.stageSize: { width, height }: Current viewport dimensions in device-independent pixels, refreshed on every stage-window resizewindow.stageReferenceSize: { width, height }: Initial stage size used to calibrate placement units and model fit. Its height defines the scale: one unpinnedScreenPlacementx/y unit renders asstageSize.height / stageReferenceSize.heightdevice-independent pixels from the viewport center
The renderer fills the window across all aspect ratios. For 2D content and perspective 3D views, width resizing reveals or crops horizontally without refitting content or letterboxing; orthographic 3D views retain a width of abs(orbit.distance). Saved placements persist across resizes and relaunches. Clients rendering placement controls should follow the current stageSize aspect ratio. Older hosts may omit these dimensions, and physical desktop window coordinates remain private.
Performance settings expose showFps, fpsLimit, effectsQuality, and renderScale (optional on older hosts). Gamepad profiles are read-only under settings; use dedicated controller.* methods to update profiles. Responses and settings.changed events also expose read-only ui.streamMode so remote panels can mask sensitive webpage URLs (omitted on older host builds).
Plugin Storage
storage.* provides persistent key-value storage scoped per API Key. All sessions sharing an API key share the same storage namespace, and revoking the key purges associated data. Values support arbitrary JSON-serializable payloads.
| Method | Description |
|---|---|
storage.get | Retrieve the value for a key; returns value: null for unwritten keys |
storage.set | Write a key-value pair |
storage.delete | Delete a key; succeeding even if the key does not exist |
storage.list | List all stored keys in lexicographical order |
Storage quotas enforce a maximum key length of 128 characters, a maximum serialized value size of 64 KB, and a ceiling of 256 keys per API key. Exceeding key count limits returns an invalid-state error.
Error Codes
When the server returns an error frame, persona.call rejects with a PersonaApiError whose code is in API_ERROR_CODES:
| Code | Description |
|---|---|
parse-error | Payload is not valid JSON |
invalid-request | Message envelope structure is malformed |
unknown-method | The method is not implemented or unavailable in the host build |
invalid-params | Parameters failed schema validation |
not-found | Referenced target identifier could not be resolved |
unsupported-for-format | The target instance format does not support the requested operation (e.g. MToon methods on a Live2D model) |
conflict | Parameter lease is currently held by another active session |
renderer-unavailable | Stage cannot respond (no window, reloading, crashed, or timed out) |
forbidden-path | Target path is outside authorized plugin directories, the user declined the asset.import prompt, or a non-loopback client called asset.import or registry.pluginFolders |
invalid-state | Operation disallowed in current state (e.g. deleting the sole scene, quota exceeded, audio failed to start, version history off) |
internal | Uncaught internal host error |
Error codes constitute an immutable protocol specification: clients discard error envelopes with unrecognized codes, so new codes are introduced only alongside major protocol revisions. New failures reuse the existing codes.
When the client or transport itself fails connect() or call(), it rejects with a PersonaClientError whose reason is in CLIENT_ERROR_REASONS:
connect()seesconnect-timeout,connection-closed, orclient-closed(or the platform's own error, such asTypeError, when theurlcannot be dialed at all, rejecting the promise rather than throwing synchronously).call()seesnot-connected,request-timeout,connection-closed, orclient-closed.
Disconnection
The client SDK automatically reconnects with exponential backoff upon unexpected network interruptions. The following WebSocket close codes indicate terminal conditions and inhibit automatic reconnection:
| Close Code | Constant | Reason |
|---|---|---|
4001 | CLOSE_KEY_REVOKED | API key was revoked |
4002 | CLOSE_FORCE_DISCONNECTED | Session was manually terminated from the Control Panel |
Upon receiving these codes, the client transitions to a terminal closed state. If configured, the onClose callback is invoked, bypassing onWarning; without onClose, the notice goes to onWarning. Standard code 1001 (Going Away) indicates normal host shutdown or restart, and the client continues reconnection attempts.
Revoking a key discards pending outbound frames immediately and closes all active connections using that key with code 4001. Pending RPC requests awaiting render pipeline confirmation are cancelled immediately. An action already forwarded to the stage may still complete, but its response is discarded.
Protocol Versioning
The wire protocol includes a major protocol version integer; if a mismatch occurs between client and server, the SDK logs a diagnostic warning. Subscribing to an event unsupported by an older Persona version does not cause a fatal error; the SDK automatically falls back to subscribing to events individually, keeping the supported ones and logging unsupported names.
The current protocol version is 4. Previous major versions streamlined and modernized protocol interfaces; plugins written for an older protocol need these migrations:
| Protocol Version | Changes |
|---|---|
| 2 | Removed Scene.behavior (cursor gaze tracking); excised from scene schema and scene.patch |
| 3 | Removed stage.shockwave; invocations return unknown-method |
| 4 | Renamed shortcut.list, shortcut.trigger, and shortcut.state to automation.list, automation.run, and automation.state; renamed shortcuts capability to automations, using automationId and automations in wire payloads |
Last updated on November 4, 2026