WebSocket API Settings
Description
The WebSocket API allows external tools to control X-Creator and receive live presentation state over a persistent WebSocket connection, instead of polling. Once connected, the server pushes the full current state (experience list, and — once joined — the current experience, its slide list, its media pool, the active slide and that slide's live media layers) and then keeps the client up to date with events as the state changes, so a client never needs to poll for changes. Live layer edits (move, resize, rotate, add, remove, …) made on the X-Creator side are pushed without requiring a save.
The API is available at ws://<ip>:<port>/ws. Requests are JSON text messages; example of a valid connection URL: ws://192.168.0.1:9698/ws
Environment Configuration
Enable the WebSocket API module so it is available for the Environment.
X-Creator Configuration
Enable the WebSocket API module for one X-Creator in the Environment that should receive the WebSocket connections and use the IP of this X-Creator when connecting.
Port
The port on which connections are accepted (default 9698)
Authentication
Enables to define a Bearer Token that must be supplied when connecting. Since browsers cannot set custom headers on a WebSocket handshake, the token is passed as a query parameter on the connection URL instead of a header: ws://192.168.0.1:9698/ws?token=<token>. A connection with a missing or invalid token is sent {"type":"error","message":"Unauthorized"} and immediately closed.
API Documentation
Message Format
Every message, in both directions, is a single-line JSON object. Incoming frames larger than 1 MiB are rejected and the connection is closed.
Commands (client → server)
{ "command": "slides/activate", "id": "<slideId>" }
command is required; id is only required by commands that operate on a specific slide, layer, media item or experience; name is only required by slides/rename; layer is only used by layers/update. The media control commands (see Media Control Commands), along with slides/timer and auto-forward/delay, use a handful of generic, reused argument fields instead of dedicated ones per command: value (number), intValue (int), boolValue (bool), text (string), color ({ "r", "g", "b", "a" }), and the strongly-typed videoPlayMode, webNavigation, ndiColorFormat, ndiBandwidth, textAlignment (each a string enum member). Which fields a command reads is documented per command below.
Responses (server → client)
{ "type": "response", "command": "slides/activate" }
Sent once per command, in reply to that command. Some commands include additional fields alongside command (see the command list below).
The slide navigation commands (slides/next, slides/previous, slides/activate) ask the server of the shared experience to change the active slide, so that all clients of that experience show the same slide. Their response confirms that the request was accepted and passed on, not that the slide has already changed. The server can decide not to carry a request out, for example when it arrives immediately after another slide change; no slide changes then, and the response is still a normal response rather than an error. Wait for the slide/changed event to see which slide actually became active.
Errors (server → client)
{ "type": "error", "command": "slides/activate", "message": "Slide 'abc' not found" }
Sent instead of a response when a command fails. command is omitted when the message itself could not be parsed (invalid JSON, or missing command field).
Events (server → client)
{ "type": "event", "event": "slide/changed", "name": "Intro", "id": "abc", "isFavorite": false }
Sent by the server on its own, without a preceding command, whenever presentation state changes. See "Events" below.
Connecting
Right after the WebSocket handshake completes, the server sends:
{"type":"event","event":"ready"}— signals that the server is now processing commands.experiences/listevent.clients/listevent.- If already joined to an experience:
experience/changed,slides/list,media/list,auto-forward/state, (if a slide is active)slide/changedandslides/current/layersevents.
The experiences/list and clients/list snapshot events are not gated on being joined to an experience — both are meaningful before that too. The rest of the set is pushed again whenever an experience is joined. A client can therefore rely entirely on this initial snapshot plus subsequent events and never has to send a command just to learn the current state.
Commands
| Command | id required | Description |
|---|---|---|
alive |
Checks that the server is responding. | |
slides/list |
Returns a list of all slides from the current experience, each with its name, ID, favorite flag and timerSeconds. |
|
slides/current |
Returns name, ID, favorite flag and timerSeconds of the current slide. |
|
slides/current/layers |
Returns the media layers of the currently active slide as Media layer objects. | |
slides/next |
Loads the next slide from the current experience. Commands arriving close together are collected into a single move covering all of them, so intermediate slides are skipped rather than each being loaded. | |
slides/previous |
Loads the previous slide from the current experience. Same collecting behavior as slides/next. |
|
slides/activate |
yes (slide ID) | Loads the slide with the given ID from the current experience. |
slides/layers |
yes (slide ID) | Returns the media layers of the slide with the given ID from the current experience as Media layer objects. |
slides/save |
Saves the currently active slide, capturing its current media layers and controls. | |
slides/rename |
yes (slide ID) + name |
Renames the slide with the given ID to name. |
slides/delete |
yes (slide ID) | Deletes the slide with the given ID. If it is the currently active slide, a neighboring slide is activated first. |
slides/timer |
yes (slide ID) + intValue |
Sets how long the slide with the given ID is shown before auto-forward moves on, in whole seconds. Zero removes the timer, the same as slides/timer/clear. No upper bound; a negative value is rejected. Setting the timer of the active slide restarts its countdown from the new duration. |
slides/timer/clear |
yes (slide ID) | Removes the timer from the slide with the given ID. The auto-forward delay still applies to it afterwards. |
auto-forward/state |
Returns the auto-forward countdown of the current experience. See Auto-forward. | |
auto-forward/delay |
no, takes intValue |
Sets the auto-forward delay of the current experience in whole seconds, applying to every slide without a timer of its own. Zero switches auto-forward off. See Auto-forward. |
auto-forward/pause |
Pauses the running countdown. Errors if no countdown is running. | |
auto-forward/resume |
Resumes the paused countdown, which carries on from where it stopped. Errors if no countdown is running, or if the presentation is frozen. | |
media/list |
Returns every media item known to the currently joined experience, independent of slide or layer placement. | |
media/layer |
yes (media ID) | Creates a new media layer for the given media on the currently active slide, positioned near the center of the visible canvas; returns the created Media layer object. |
layers/get |
yes (layer ID) | Returns the media layer with the given ID on the currently active slide as a Media layer object, control state included. Refreshes one layer — including a playing video's position — without pulling the whole slide. |
layers/update |
yes (layer ID) + layer |
Applies a partial update to the media layer with the given ID on the currently active slide. The layer field holds a Media layer object with only the properties to change (see Updating a layer). |
layers/delete |
yes (layer ID) | Deletes the media layer with the given ID from the currently active slide, including its control state. |
experiences/list |
Returns a list of all experiences with names, IDs and if set as favorites. | |
experiences/current |
Returns name, ID, favorite flag and canvas dimensions of the current experience. | |
experiences/slides |
yes (experience ID) | Returns a list of all slides for the experience with the given ID, without joining it. If this X-Creator hasn't loaded that experience yet, it is fetched from X-Manager on demand. Only available on a X-Creator running the Host role. |
experiences/join |
yes (experience ID) | Joins the experience with the given ID. If the experience already has a client with sync enabled, this client joins with sync forced off, matching the app's own join behavior. |
experiences/leave |
Leaves the current experience. Errors if no experience is currently loaded. | |
clients/list |
Returns every connected client as Client objects. | |
clients/current |
Returns the client answering this request as a Client object. | |
clients/get |
yes (client ID) | Returns the client with the given ID as a Client object. |
shortcuts/trigger |
no, takes shortcut |
Fires an app-side shortcut, exactly as pressing its key combination would. See Triggering a shortcut. |
shortcuts/get |
no, takes shortcut |
Returns the current state of a shortcut that has readable state. See Reading shortcut state. |
Media-type-specific operations (play/pause, page navigation, etc.) are listed separately in Media Control Commands.
Any unrecognized command value returns {"type":"error","command":"<command>","message":"Unknown command"}.
Response payloads
Successful responses carry their data alongside type and command as follows:
| Command | Payload |
|---|---|
slides/list |
array under slides |
slides/current/layers |
array under layers |
slides/layers |
array under layers, plus the requested id echoed |
media/list |
array under media |
media/layer |
created layer object under layer |
layers/get |
layer object under layer, plus the requested id echoed |
experiences/list |
array under experiences |
experiences/slides |
array under slides, plus the requested id echoed |
slides/current |
name, id, isFavorite, timerSeconds inline on the envelope |
auto-forward/state |
slideId, running, durationMs, remainingMs, delaySeconds inline on the envelope |
experiences/current |
name, id, isFavorite, canvasWidth, canvasHeight inline on the envelope |
shortcuts/get |
enabled inline on the envelope |
clients/list |
array under clients |
clients/current |
Client object fields inline on the envelope |
clients/get |
client object under client, plus the requested id echoed |
All other commands (alive, navigation, save, rename, delete, join, leave, updates, shortcut triggers) reply with just { "type": "response", "command": "<command>" }.
Media layer object
The layer commands and events (slides/current/layers, slides/layers, media/layer, layers/update, and the slides/current/layers event) describe each media layer with this object. Transform values are normalized rather than in pixels:
| Field | Type | Description |
|---|---|---|
mediaLayerId |
string | Unique ID of the layer. |
mediaId |
string | ID of the media the layer displays. |
mediaType |
string | Media type, e.g. Image, Video, Text, Web, AssetBundle, Shape. |
name |
string | Display name of the media. |
position |
{ "x", "y" } |
Normalized position. x spans the canvas width, y spans the canvas height (0–1 covers the canvas). |
size |
{ "x", "y" } |
Normalized size. Both x (width) and y (height) are relative to the canvas width. |
rotation |
number | Rotation in degrees. |
crop |
{ "x", "y", "z", "w" } |
Crop fractions (0–1) per edge: x=top, y=bottom, z=left, w=right. Also adjusts position/size to match, unless those are set in the same request. |
visible |
bool | Whether the layer is shown. |
alpha |
number | Opacity, 0–1. |
zOrder |
int | Stacking order; higher values are drawn in front. Relative only — not guaranteed 0-based or contiguous, and values differ between the active slide (normalized) and other slides (raw stored values). |
locked |
bool | Whether the layer is locked against edits. |
fixedAspectRatio |
bool | Whether width and height stay locked to the media's aspect ratio. |
fullscreen |
bool | Whether the layer fills the whole canvas. |
sectionFullscreen |
bool | Whether the layer fills its section. Mutually exclusive with fullscreen. |
backgroundColor |
{ "r", "g", "b", "a" } |
Background color behind the media, each channel 0–1. |
isStatic |
bool | Whether the layer is static (persists across every slide). Toggling this also re-maps layerGroup to/from its Static* variant. |
layerGroup |
string | Layer group: Background, Regular, Foreground, StaticBackground, StaticRegular, StaticForeground. Setting a non-static base group (e.g. Foreground) while the layer is static resolves to its Static* counterpart automatically. System-assigned groups (Widget, VideoSource, Audio) can't be set through the API. |
control |
object or null |
Media-type-specific control state snapshot, shaped per mediaType — see Media control object. null for media types with no control operations (e.g. Image, AssetBundle, VideoMatrix). |
Media control object
Every response/event that includes a Media layer object nests the layer's current control state under control, shaped according to its mediaType. To read it for a single layer, use the layers/get command.
mediaType |
Fields |
|---|---|
Video |
isPlaying (bool), playMode (string: Loop, ResetAfterPlay, PlayToEnd, ForwardSlideWhenFinished), volume (0–1), muted (bool), position (0–1, normalized over duration), duration (seconds) |
Audio |
isPlaying (bool), loop (bool), volume (0–1), muted (bool), position (0–1, normalized over duration), duration (seconds) |
duration is the media's total length in seconds, so position * duration gives the elapsed time — enough to show and seek to a real timecode. It is 0 when the length isn't known (media without duration metadata).
| Pdf | currentPage (int, 0-based), interaction (bool) |
| Web | webUrl (string) |
| Ndi | volume (0–1), muted (bool), colorFormat (string: BGRX_BGRA, UYVY_BGRA, RGBX_RGBA, UYVY_RGBA, Fastest), bandwidth (string: Highest, Lowest, AudioOnly) |
| Text | text (string), fontName (string), fontSize (number), fontColor ({ "r", "g", "b", "a" }), alignment (string, Unity TextAnchor member, e.g. UpperLeft), interaction (bool) |
| Shape | color ({ "r", "g", "b", "a" }) |
| everything else | null — no control operations are exposed for this media type |
Client object
The client commands and events (clients/list, clients/current, clients/get) describe a connected client with this object, sourced from the server's internal client-info state rather than a separate schema per client type:
| Field | Type | Description |
|---|---|---|
clientId |
string | Unique ID of the client. |
clientName |
string | Display name of the client, or empty if unset. |
clientType |
string | Client role, e.g. Visualisation, Control. |
connectionId |
int | Internal network connection handle, mainly useful for debugging. Not stable across reconnects. |
syncEnabled |
bool | Whether this client follows another client's slide navigation. |
experienceId |
string | ID of the experience this client has joined, or empty if none. |
slideId |
string | ID of the slide this client currently has active, or empty if none. |
slideLoadState |
string | How far the client has got loading slideId, e.g. None, Loading, Settled. |
autoPlayDelayMs |
int | Configured auto-forward delay for this client in milliseconds, or 0 if auto-play is off. |
downloadProgress |
int | Media download progress, 0–100. |
ndi |
object | enabled (bool), mediaIds (array of string) — whether NDI is enabled on this client and which media it currently sources over NDI. |
hardwareInfo |
object | deviceType (string, Unity DeviceType member), deviceName (string), systemMemorySize (int, MB), processorFrequency (int, MHz), graphicsMemorySize (int, MB). |
The list is only as current as the server's own broadcast — a Control client reads the same continuously-synced copy as the host, so the commands and the clients/list event work the same regardless of which client answers.
Auto-forward
Auto-forward moves the presentation on by itself. How long a slide is shown is resolved from two settings: the slide's own timerSeconds if it has one, otherwise the experience's auto-forward delay, which applies to every slide without a timer.
The auto-forward/state response and event carry the countdown:
| Field | Type | Description |
|---|---|---|
slideId |
string | The slide the countdown is running for, empty when none is running. |
running |
bool | Whether the countdown is running rather than paused. |
durationMs |
int | How long the countdown runs in total. |
remainingMs |
int | How much was left when the event was sent. |
delaySeconds |
number | The auto-forward delay, 0 when auto-forward is off. Fractional when it comes from a client configuration that is not a whole number of seconds. |
The countdown does not tick over the network — auto-forward/state is only pushed when it starts, stops, is paused or resumed, or when the delay changes. A client wanting a live readout counts remainingMs down against its own clock from the moment the event arrived, exactly as the app's own timer ring does.
Pause and resume are unowned: a countdown paused through the API is resumed by the app's own timer ring just as readily as by auto-forward/resume, and vice versa. Naming the operation rather than toggling also makes it idempotent — sending auto-forward/pause twice succeeds and changes nothing. Pausing is allowed while the presentation is frozen (freeze already holds the countdown, and pausing on top of it is what keeps it from resuming when freeze is lifted); resuming while frozen is rejected.
auto-forward/delay overrides the delay taken from client configuration for as long as the server runs; a restart falls back to client configuration, and nothing is written to any configuration file. Zero switches auto-forward off — there is no separate enable/disable and no remembered previous value. A countdown already running for a slide without its own timer restarts from the new delay, as it does when that slide's own timer is changed.
There is no restart command: reloading the slide restarts its countdown.
Media Control Commands
All commands below take id (a mediaLayerId) and reply with just { "type": "response", "command": "<command>" } on success, or an error using the same conditions as other layer commands (Missing id, Media layer '<id>' not found, or a "has no X control state" message when the layer's mediaType doesn't support the operation).
Scope — some operations apply only to the targeted layer instance ("Layer"); others apply to every layer currently showing the same media item ("Media"), e.g. muting one placement of a video mutes all of them. A control-state change (from any client) also triggers a slides/current/layers event, same as a transform edit.
Volume and mute — the video/volume, audio/volume, and ndi/volume commands also set mute, mirroring the volume slider in the app: a volume above 0 unmutes, a volume of 0 mutes. The app's slider reads as 0 for as long as mute is set, whatever volume is behind it, so a volume change that left mute untouched would not be visible there. Send */mute after */volume if the two need to be set independently.
Video (all Media-scoped)
| Command | Fields | Description |
|---|---|---|
video/play |
boolValue (isPlaying) |
Play or pause at the media's current position; never seeks. |
video/seek |
value (position) |
Seek without changing play state. 0–1, normalized over the media's duration. |
video/mode |
videoPlayMode |
Loop, ResetAfterPlay, PlayToEnd, or ForwardSlideWhenFinished. |
video/volume |
value |
0–1. Also unmutes, or mutes at 0. |
video/mute |
boolValue |
Audio (all Layer-scoped)
| Command | Fields | Description |
|---|---|---|
audio/play |
boolValue (play) |
Play or pause at the media's current position; never seeks. |
audio/position |
value |
Seek. 0–1, normalized over the media's duration. |
audio/loop |
boolValue |
|
audio/volume |
value |
0–1. Also unmutes, or mutes at 0. |
audio/mute |
boolValue |
Pdf (all Layer-scoped)
| Command | Fields | Description |
|---|---|---|
pdf/page |
intValue (page) |
Jump to a 0-based page number. |
pdf/next-page |
none | Advance one page, clamped to the last page. |
pdf/previous-page |
none | Go back one page, clamped to page 0. |
pdf/first-page |
none | Jump to page 0. |
pdf/last-page |
none | Jump to the last page. |
pdf/interaction |
boolValue |
Toggles direct interaction (scrolling) with the PDF on the layer. |
pdf/enable-scrolling |
none | Re-enables dispatching the current page while scrolling manually. |
Web (Layer-scoped)
| Command | Fields | Description |
|---|---|---|
web/url |
text (url) |
Loads a new URL. |
web/navigate |
webNavigation |
Back, Forward, or Reload. |
Ndi (all Media-scoped)
| Command | Fields | Description |
|---|---|---|
ndi/volume |
value |
0–1. Also unmutes, or mutes at 0. |
ndi/mute |
boolValue |
|
ndi/color-format |
ndiColorFormat |
BGRX_BGRA, UYVY_BGRA, RGBX_RGBA, UYVY_RGBA, or Fastest. |
ndi/bandwidth |
ndiBandwidth |
Highest, Lowest, or AudioOnly. |
Text
| Command | Scope | Fields | Description |
|---|---|---|---|
text/text |
Media | text |
|
text/font |
Layer | text (fontName) |
|
text/font-size |
Layer | value (fontSize) |
|
text/font-color |
Layer | color |
|
text/alignment |
Layer | textAlignment |
Unity TextAnchor member, e.g. UpperLeft, MiddleCenter. |
text/interaction |
Layer | boolValue |
Shape (Layer-scoped)
| Command | Fields | Description |
|---|---|---|
shape/color |
color |
Not exposed: freeze (any media type), NDI PTZ (ndi/ptz/*) and camera presets, Web scale/interaction/interaction-authority, and AssetBundle/VideoMatrix controls entirely.
To convert normalized values to pixels, use the canvasWidth / canvasHeight fields from the experience/changed event or experiences/current response (default 1920×1080):
xPx = position.x * canvasWidth, yPx = position.y * canvasHeight, widthPx = size.x * canvasWidth, heightPx = size.y * canvasWidth.
Updating a layer
layers/update carries a layer object containing only the properties to change; omitted properties are left untouched. At least one property must be present, otherwise the command errors with Missing layer properties.
{ "command": "layers/update", "id": "<layerId>", "layer": { "position": { "x": 0.25, "y": 0.5 } } }
Notes:
position,sizeandcropare sent whole (all components), not per-axis.rotationis an absolute value in degrees; the server applies the difference from the current rotation.- Setting
fullscreentotrueclearssectionFullscreenand vice versa (they are mutually exclusive). - Errors:
Missing id,No active slide,Media layer '<id>' not found,Missing layer properties, orLayer group '<value>' cannot be set directly(system-assigned or unknownlayerGroup).
Triggering a shortcut
shortcuts/trigger takes a shortcut name instead of an id and fires that shortcut's handlers, exactly as pressing its key combination on the X-Creator would. No experience needs to be joined.
{ "command": "shortcuts/trigger", "shortcut": "MouseCursor" }
Only these three shortcuts are exposed:
shortcut |
Key combination | Effect |
|---|---|---|
MouseCursor |
Ctrl + M |
Shows or hides the mouse cursor. |
ShowWelcomeScreen |
Ctrl + W |
Shows or hides the welcome screen. |
ForceWindowFocus |
Alt + Shift + W |
Turns forcing focus on the X-Creator window on or off. |
MouseCursor and ShowWelcomeScreen are toggles that report no state, so this command can only flip them. ForceWindowFocus is the exception — read its resulting state back with Reading shortcut state below.
Errors: Missing or unknown shortcut, or Shortcut '<name>' cannot be triggered remotely for a valid shortcut that is not in the list above.
Reading shortcut state
shortcuts/get takes a shortcut name and returns its current state. Only ForceWindowFocus supports this — MouseCursor and ShowWelcomeScreen are pure toggles with nothing to read back.
{ "command": "shortcuts/get", "shortcut": "ForceWindowFocus" }
Response payload: enabled (boolean), inline on the envelope.
Errors: Only 'ForceWindowFocus' has readable state for any other shortcut name, or Force window focus watcher is not active on this instance when the watcher isn't installed (editor, non-Windows, or server role).
Events
Events are pushed by the server without a matching command; a client should apply them to its local state as they arrive.
| Event | Description |
|---|---|
ready |
Sent once right after connecting; the state snapshot described above follows immediately after. |
experiences/list |
The list of accessible experiences. Sent in the connect snapshot and re-sent whenever the list changes. |
clients/list |
The list of connected clients as Client objects. Sent in the connect snapshot and re-sent whenever any client's info changes — not gated on being joined to an experience. |
experience/state |
Informational only; state is joining or leaving while a join/leave is in progress. Does not drive client state by itself. |
experience/changed |
Sent once an experience join completes, together with the rest of the joined-experience snapshot. Includes canvasWidth and canvasHeight (pixels) for converting normalized layer values. |
experience/left |
Sent once the client has left its experience. |
slides/list |
The slide list of the currently joined experience; sent on join and re-sent whenever a slide is added, removed, renamed or otherwise changes. |
slide/changed |
Sent whenever the active slide changes, including right after a join completes. |
media/list |
The media pool of the currently joined experience; sent on join and re-sent whenever media is added or removed. |
auto-forward/state |
The auto-forward countdown; sent on join and on every transition, never on a tick. See Auto-forward. |
slides/current/layers |
The active slide's media layers as Media layer objects; sent on join, on slide change, and live as layers are edited. Continuous edits (e.g. a drag) are coalesced to at most one event per second. |
Samples
Connect and load the next slide:
{ "command": "slides/next" }
Get the media layers of the current slide:
{ "command": "slides/current/layers" }
Rename a slide:
{ "command": "slides/rename", "id": "<slideId>", "name": "New name" }
Move a layer and set its opacity (only the given properties change):
{ "command": "layers/update", "id": "<layerId>", "layer": { "position": { "x": 0.5, "y": 0.5 }, "alpha": 0.8 } }
Tools
Built-in Test Console
Opening http://<ip>:<port>/ (the same host and port as the WebSocket endpoint) in a browser serves a built-in interactive console for connecting, sending commands and inspecting responses/events without any external tooling.
Troubleshooting
Connection is closed immediately after connecting
If Authentication is enabled, the connection URL is missing a valid ?token=<token> query parameter. Check the Bearer Token configured for the X-Creator.
Command returns {"type":"error", ...}
The message field describes why the command could not be completed, e.g. a slide or experience ID that does not exist, or navigating past the last/first slide. This mirrors the equivalent REST API error conditions.
No events are received after connecting
Confirm the ready event was received first — if not, the handshake or authentication failed. If ready was received but no experience-specific events follow, no experience is currently joined; join one with experiences/join first.