REST API Settings
Description
REST API allows external tools to control X-Creator using HTTP requests (including CORS support). All API requests have to be prefixed with the IP address of the X-Creator, the configured port, "api" and the API version. Example of a valid API request: http://192.168.0.1:9696/api/v1/experiences
Environment Configuration
Enable the REST API module so it is available for the Environment.
X-Creator Configuration
Enable the REST API module for one X-Creator in the Environment that should receive the REST requests and use the IP of this X-Creator when sending the requests.
Port
The port on which the requests are received
Authentication
Enables to define a Bearer Token (case sensitive) that must be added to the header of each request to access all endpoints.
'Bearer' has to be capitalized in the authorization header.
X-Web Control

Configuration
- By starting the application, a default config file is created next to the executable if no config file found
- Set values in
config.jsonbefore starting the application to start with desired configuration (IPs, ports and crestron endpoints)
Warning
Do not position the executable inside of a directory with admin rights (recommend directory e.g.: C:\Users\Public\Documents)
Config Sample File
The following sample can be used as a starting point and copy-pasted to the config.json file. It contains all default values.
{
"WEB_CONTROL_PORT": "9697",
"CREATOR_REST_API_IP": "http://192.168.0.100",
"CREATOR_REST_API_PORT": "9696",
"PIN_CODE": "9999",
"CRESTRON_IP": "http://192.168.0.200",
"CRESTRON_HOME_PATH": "/home",
"CRESTRON_X_PANEL_URL": "https://192.168.1.11/shell-template/index.html?ipId=11"
}
API Documentation
All endpoints require a valid bearer token in the Authorization header when Authentication is enabled; every response below can additionally return 401 in that case.
Error response body
Every error response (4xx/5xx) carries a JSON body with the status code and a human-readable reason, matching the shape of successful responses:
{ "status": 404, "message": "No experience with specified ID found" }
Media layer object
The layer endpoints (/slides/{id}/layers, /slides/current/layers, /media/{mediaId}/layer) 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). |
The canvas dimensions needed to convert normalized values to pixels are returned by /experiences/current as canvasWidth and canvasHeight. Individual layer properties can be updated live with PATCH /layers/{id}; media-type-specific operations (play/pause, page navigation, etc.) are covered in Media Control Endpoints.
Media control object
Every response 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 GET /layers/{id}.
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 endpoints (/clients, /clients/current, /clients/{id}) 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. |
address |
string | Address this client can be reached at, as the host observes it on its connection. Empty on a client that has not registered with a host yet. Together with restPort this is what lets a caller address another client's own API without being told where it runs. |
restPort |
int | Port this client serves its REST API on, or 0 when the REST API is switched off for it (or has not started yet). |
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 all three endpoints work the same regardless of which client answers the request.
Chapter object
The chapter endpoints (/chapters, /chapters/{id}, /experiences/{id}/chapters) describe a chapter with this object. A chapter is an ordered group of slides; every slide in an experience belongs to exactly one chapter.
| Field | Type | Description |
|---|---|---|
name |
string | Display name of the chapter. |
id |
string | Unique ID of the chapter. |
slides |
array | The chapter's slides, in order, each as { "name", "id", "isFavorite" } (same shape as /slides, without timerSeconds). |
X-Manager object
The /x-manager endpoint describes the X-Manager this X-Creator is connected to with this object:
| Field | Type | Description |
|---|---|---|
url |
string | Address of the X-Manager as configured in X-Creator, a full base URL with scheme, host and port (e.g. https://x-manager.example.com:3001) rather than a bare IP. |
version |
string | Release version of the X-Manager, e.g. 2023.2.279 (develop). Empty until the X-Manager has been reachable at least once. |
publicFeatureEnabled |
bool | Whether the public feature is enabled on the X-Manager. Read from the X-Manager's own settings, not inferred from whether X-Creator currently holds a public token. false until the X-Manager has been reachable at least once. |
version and publicFeatureEnabled are read from the X-Manager when the backend connection comes up and re-read on every reconnect, so a setting changed on the X-Manager is picked up without restarting X-Creator. Every X-Creator reads them for itself, so the answer does not depend on which instance is asked.
Version object
The /version endpoint describes the running build with this object:
| Field | Type | Description |
|---|---|---|
version |
string | Application version including the build number, e.g. 2023.2.279. |
branch |
string | Branch the build came from, e.g. develop or staging. Empty for a production build, and for a locally made build, which carries no build manifest. |
commit |
string | Commit the build came from. Empty when no build manifest is present. |
buildTime |
string | When the build was made. Empty when no build manifest is present. |
buildKind |
string | production, or agent-api for a build carrying the extra unauthenticated agent interface. Decided at compile time, so a production build answers this too rather than the question only being answerable where that interface exists. |
Config object
The /config endpoint describes the X-Creator configuration this instance runs under with this object. Everything here is per instance: each X-Creator is served its own configuration, so ask the instance you mean rather than the host.
| Field | Type | Description |
|---|---|---|
configId |
string | ID of the X-Creator configuration, as registered on the X-Manager. Reflects whatever was last selected (welcome screen or POST /config), not re-fetched by this endpoint. |
configName |
string | Name of that configuration. This is the same value the Client object reports as clientName. Same caveat as configId. |
environmentId |
string | ID of the environment the configuration belongs to. |
environmentName |
string | Name of that environment. Empty until the X-Manager has served this instance its client info at least once, since only the X-Manager knows the name behind the id. |
source |
string | Whether environmentId/environmentName came from the X-Manager this run: x-manager when it served them, cached when it could not be reached and the previous run's values were kept, unknown before either has happened. Does not describe configId/configName, which this endpoint never re-fetches. |
source is worth reading before trusting the environment fields: startup deliberately carries on with the previous run's environment info when the X-Manager is unreachable, and it looks no different from a freshly-served value once in place. Since both requests are keyed only by client id, resolved independently server-side, a cached environment is not just possibly stale but possibly wrong for what this instance is actually running — the X-Manager can have reassigned this client to a different environment in the meantime, and the license/config the app is running under would already reflect that even though environmentName here still doesn't.
Config entry object
The /configs endpoint describes each X-Creator configuration the connected X-Manager offers with this object. Unlike the Config object this says nothing about what the asked instance is running — it is the list to pick from, not a report about the instance.
| Field | Type | Description |
|---|---|---|
configId |
string | ID of the X-Creator configuration, as registered on the X-Manager. Not accepted by POST /config, and not stable across a reimport. |
configName |
string | Name of that configuration. This, paired with environmentName, is what POST /config selects by. |
environmentId |
string | ID of the environment the configuration belongs to. |
environmentName |
string | Name of that environment. Needed alongside configName to address a configuration, since names are only unique within one environment. |
Environment entry object
The /environments endpoint describes each environment the connected X-Manager holds with this object. It exists so an environment name can be turned into the id the administrator operations take.
| Field | Type | Description |
|---|---|---|
environmentId |
string | ID of the environment on the X-Manager. Not stable across a reimport of its content. |
environmentName |
string | Name of that environment. |
This is not the same set as the environments appearing in GET /configs: that only reaches environments which already have a configuration, and an environment with none is exactly what a configuration is usually moved to.
License object
The /licenses/{licenseId}/environment endpoint describes the license it changed with this object. It is deliberately a small part of what the X-Manager holds about a license: the key and the hardware id it is bound to stay on the X-Manager and are never served here.
| Field | Type | Description |
|---|---|---|
licenseId |
string | ID of the license on the X-Manager. |
licenseName |
string | Name of that license. |
environmentId |
string | ID of the environment the license now applies to. |
environmentName |
string | Name of that environment. Looked up separately when the license document does not carry it, and null if that lookup failed. |
isValid |
boolean | Whether the license covers the environment it now belongs to. false does not mean the change failed — the move succeeds either way, but an X-Creator on that environment then fails to start with an invalid-license error. |
Log manifest object
The /logs endpoint answers with a zip rather than JSON, and writes this object into it as manifest.json. Its point is that triage can tell a complete bundle from one with a gap: which machine the logs came from, which window was asked for, and which files answered it — rather than working with whatever files happen to be in the archive.
| Field | Type | Description |
|---|---|---|
clientId |
string | ID of the X-Creator configuration the exporting instance runs under, so a bundle stays identifiable once it is off the test network. |
clientName |
string | Name of that configuration. |
generatedAt |
string | When the bundle was produced, ISO 8601 in UTC — as is every other time in this object, whatever the request used. |
windowFrom |
string | Start of the requested window, null when none was given. |
windowTo |
string | End of the requested window, null when none was given. |
limit |
integer | The requested file limit, 0 when none was given. |
fileCount |
integer | Number of files listed. |
totalSizeBytes |
integer | Their combined size on disk. |
files |
array | One entry per selected file, oldest first. |
Each entry in files:
| Field | Type | Description |
|---|---|---|
name |
string | File name, which is also its name inside the zip. |
startTime |
string | When the run that wrote the file started, read from the file name. null if the name does not carry one. |
lastWriteTime |
string | When the file was last written to. This, not startTime, is what says how far into the window the file reaches. |
sizeBytes |
integer | Size on disk at selection time. |
stillOpen |
boolean | true for the file the application is writing to right now — the most interesting file in a bundle, and the only one that can still grow after the export. |
error |
string | Why a listed file is not in the zip, for instance because log rotation replaced it while the bundle was being built. null for every file that made it in. |
A file listed with an error is the case the manifest exists for: the bundle is short one file and says so, instead of looking complete.
Administrator operations
Two endpoints — POST /configs/{configId}/environment and POST /licenses/{licenseId}/environment — do not change this instance but the X-Manager's own records, so that an automated setup can move an X-Creator between environments without anyone opening the X-Manager UI.
Because of that they need more than this API's bearer token: the X-Creator forwards them to the X-Manager under the credential of the user logged in on this instance, and the X-Manager only accepts them from a member of its Administrators group. There is no second credential to fall back on, so an instance that is not logged in as such a user answers 403 and names which of these applies:
- it accesses the X-Manager with an API token, which is minted without administrator rights,
- no X-Manager user is logged in on it — including when it runs in public mode,
- or the logged-in user is not in the Administrators group.
A 403 can also come back from the X-Manager itself after that check passed, for instance when the user's group membership changed since the login.
/alive
GET
Description
Is the REST server alive.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
/x-manager
GET
Description
Returns the X-Manager this X-Creator is connected to as an X-Manager object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | X-Manager document | object |
| 401 | Authentication token not valid |
/version
GET
Description
Returns the running build as a Version object. Answers which of several installed builds is actually running on this machine, which cannot be told from the outside once a production and an agent-API build of the same version sit side by side.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Version document | object |
| 401 | Authentication token not valid |
/config
GET
Description
Returns the X-Creator configuration this instance runs under as a Config object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Config document | object |
| 401 | Authentication token not valid |
POST
Description
Selects the X-Creator configuration this instance runs under, taking effect on the next start. Returns the resulting Config object.
Selection is by name, not by id, because the X-Manager mints new ids whenever its content is reimported while names stay put. environmentName is required as well, since configuration names are only unique within one environment. The name pair is resolved against the X-Manager's own list, so this needs the X-Manager to be reachable — a 500 means it was not, and is a different problem from the 404 that says the name does not exist there.
This endpoint deliberately does not restart the application. Nothing is torn down at the moment of the call; the caller decides when the restart happens. Until it does, the instance keeps running under its previous configuration while reporting the new one.
Request body
{ "configName": "Visualisation Left", "environmentName": "Showroom" }
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Config document, as resolved | object |
| 400 | configName or environmentName missing |
|
| 401 | Authentication token not valid | |
| 404 | No configuration with that name in that environment on this X-Manager | |
| 500 | The configuration list could not be read from the X-Manager |
/configs
GET
Description
Returns every X-Creator configuration the connected X-Manager offers, as a list of Config entry objects — the names POST /config can be given.
The list comes from the X-Manager itself rather than from anything held locally, so this needs it to be reachable and a 500 means it was not. It describes what this X-Manager offers, not what any instance is running; use GET /config for the latter.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Config entry documents | object |
| 401 | Authentication token not valid | |
| 500 | The configuration list could not be read from the X-Manager |
/environments
GET
Description
Returns every environment the connected X-Manager holds, as a list of Environment entry objects — where the environmentId for POST /configs/{configId}/environment and POST /licenses/{licenseId}/environment comes from.
Unlike those two this is a plain read and needs no administrator credential, only whatever the X-Manager itself requires to list environments; a 403 means it refused the read.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Environment entry documents | object |
| 401 | Authentication token not valid | |
| 403 | The X-Manager refused to serve the environment list | |
| 500 | The environment list could not be read from the X-Manager |
/licenses
GET
Description
Returns every license the connected X-Manager holds, as a list of License objects — where the licenseId for POST /licenses/{licenseId}/environment comes from, and which shows which environment is currently licensed.
Like GET /environments this is a plain read and needs no administrator credential. License keys are not part of the response.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | License documents | object |
| 401 | Authentication token not valid | |
| 403 | The X-Manager refused to serve the license list | |
| 500 | The license list could not be read from the X-Manager |
/configs/{configId}/environment
POST
Description
Moves an X-Creator configuration to another environment on the X-Manager, so that instances running this configuration are served the new environment's settings from their next start. Returns the updated Config entry object.
This is an administrator operation. Unlike POST /config it does not change what the asked instance runs — it changes a record on the X-Manager, which every instance on that configuration then follows.
Addressing is by id rather than by name, because a configuration and an environment are each identified by id in the X-Manager's own records. Read the configuration ids from GET /configs and the environment ids from GET /environments; neither survives a reimport of the X-Manager's content.
A configuration belongs to exactly one environment, which is why this is a separate operation rather than a field on POST /config. The environment must already be licensed, or an X-Creator moved onto it fails to start — see POST /licenses/{licenseId}/environment.
environmentName in the response is resolved separately, because the X-Manager's answer to this call names the environment by id only. It is null if that lookup failed — the move itself has already happened by then and is not reported as failed over it.
Request body
{ "environmentId": "6634f1b2c8a94d0012ab34cd" }
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Config entry document, as updated | object |
| 400 | configId or environmentId missing |
|
| 401 | Authentication token not valid | |
| 403 | No X-Manager administrator credential on this instance, or the X-Manager refused the change | |
| 404 | No configuration with that id on this X-Manager | |
| 500 | The X-Manager could not be reached, or answered unreadably |
/licenses/{licenseId}/environment
POST
Description
Moves a license to another environment on the X-Manager. Returns the updated License object.
This is an administrator operation. An environment without a license cannot be started on: an X-Creator moved to it fails during startup with an invalid-license error rather than a configuration error.
A license record carries one environment, so this re-points it rather than copying it: the environment it was on loses this license. Whether that environment ends up unlicensed depends on whether another license still covers it — an installation can hold several. A trial is the exception: there is only one per installation, so it cannot cover two environments and has to be moved between them.
The license does not have to be deactivated first — posting the new environment id is enough, and the license comes back valid. The license key is never part of the request or the response.
Request body
{ "environmentId": "6634f1b2c8a94d0012ab34cd" }
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | License document, as updated | object |
| 400 | licenseId or environmentId missing |
|
| 401 | Authentication token not valid | |
| 403 | No X-Manager administrator credential on this instance, or the X-Manager refused the change | |
| 404 | No license with that id on this X-Manager | |
| 500 | The X-Manager could not be reached, or answered unreadably |
/logs
GET
Description
Returns this machine's log files for a time window as a zip, with a Log manifest object inside it as manifest.json. Meant to be called at the end of a test run, from the machine that produced the logs — by triage time they have rotated away or the machine has been reused.
Files are selected by overlap, not by name. A log file is named for the time its run started, so a run that lasted all morning holds entries hours later than the time in its name; a file counts as in the window when it was still being written at or after from, and its run began at or before to. The file the application is currently writing to is included like any other and flagged in the manifest, and buffered entries are flushed to disk first so the last seconds before the call are in the bundle. Expect several files per machine when a test case restarts the application.
Everything is optional: no parameters at all returns every log file the machine has kept — roughly the last 100 runs.
Query parameters
| Name | Type | Description |
|---|---|---|
from |
string | Start of the window. Either an ISO 8601 timestamp (without an offset it is read as the machine's local time) or a duration back from now: 10d, 12h, 30m, 45s. Omitted, the window is open at the start. |
to |
string | End of the window, same formats. Omitted, the window is open at the end. |
limit |
integer | Keep at most this many files, newest first. Omitted, every matching file is included. |
Examples
| Request | Returns |
|---|---|
GET /logs |
Every retained log file. |
GET /logs?from=10d |
The last ten days. |
GET /logs?limit=1 |
The current log file only. |
GET /logs?from=2026-09-16T08:00:00&to=2026-09-16T12:00:00 |
Everything overlapping that morning. |
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Zip archive of the selected log files plus manifest.json. A window matching nothing is still a 200 with a manifest listing no files, not a 404. |
binary |
| 400 | from, to or limit not readable |
|
| 401 | Authentication token not valid |
/experiences
GET
Description
Returns a list of all experiences with names, IDs and if set as favorites.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Experience documents | object |
| 401 | Authentication token not valid |
/experiences/{id}
GET
Description
Loads the experience with a 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 — this avoids disturbing clients already
synced to that experience.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 404 | No experience with specified ID found |
/experiences/{id}/slides
GET
Description
Returns a list of all slides (name, ID, favorite) 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.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Slide documents | object |
| 401 | Authentication token not valid | |
| 404 | No experience with specified ID found | |
| 500 | Fetching the experience from X-Manager failed | |
| 501 | This X-Creator is not running the Host role |
/experiences/{id}/chapters
GET
Description
Returns a list of all chapters, each with its 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. See Chapter object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Chapter documents | object |
| 401 | Authentication token not valid | |
| 404 | No experience with specified ID found | |
| 500 | Fetching the experience from X-Manager failed | |
| 501 | This X-Creator is not running the Host role |
/experiences/exit
GET
Description
Leave current experience. Has no effect if no experience is currently loaded.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
/experiences/current
GET
Description
Returns name and ID of the current experience, plus the canvas pixel dimensions (canvasWidth, canvasHeight) used to convert normalized Media layer object values to pixels.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Experience document (name, id, isFavorite, canvasWidth, canvasHeight) |
object |
| 401 | Authentication token not valid | |
| 404 | No experience loaded |
/clients
GET
Description
Returns every connected client as a Client object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Client documents | object |
| 401 | Authentication token not valid |
/clients/current
GET
Description
Returns the client answering this request as a Client object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Client document | object |
| 401 | Authentication token not valid |
/clients/{id}
GET
Description
Returns the client with the given ID as a Client object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Client document | object |
| 401 | Authentication token not valid | |
| 404 | No client with specified ID found |
/slides
GET
Description
Returns a list of all slides from the current experience.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Slide documents (name, id, isFavorite, timerSeconds) — empty list if no experience is loaded |
object |
| 401 | Authentication token not valid |
/slides/{id}
GET
Description
Loads the slide with a given ID from the current experience.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 404 | No slide with specified ID found in the current experience |
DELETE
Description
Deletes the slide with a given ID from the current experience. If it is the currently active slide, a neighboring slide is activated first.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 404 | No slide with specified ID found in the current experience |
| 409 | Cannot delete the last remaining slide |
/slides/{id}/layers
GET
Description
Returns the media layers of the slide with the given ID from the current experience as Media layer objects.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Media layer documents | object |
| 401 | Authentication token not valid | |
| 404 | No slide with specified ID found in the current experience |
/chapters
GET
Description
Returns a list of all chapters, each with its slides, from the current experience. See Chapter object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Chapter documents — empty list if no experience is loaded | object |
| 401 | Authentication token not valid |
/chapters/{id}
GET
Description
Returns the chapter with a given ID from the current experience. See Chapter object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Chapter document | object |
| 401 | Authentication token not valid | |
| 404 | No chapter with specified ID found in the current experience |
/chapters/{id}/activate
POST
Description
Loads the first slide of the chapter with a given ID from the current experience.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 404 | No chapter with specified ID found in the current experience |
| 409 | Chapter has no slides |
/slides/next
GET
Description
Loads the next slide from the current experience. Several calls arriving in quick succession are
collected into a single move covering all of them, so the intermediate slides are skipped rather than
each being loaded in turn — see Slide navigation is a request.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 409 | No next slide available |
/slides/previous
GET
Description
Loads the previous slide from the current experience. Several calls arriving in quick succession are
collected into a single move covering all of them, so the intermediate slides are skipped rather than
each being loaded in turn — see Slide navigation is a request.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 409 | No previous slide available |
/slides/current
GET
Description
Returns name, ID and favorite flag of the current slide.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Slide document (name, id, isFavorite, timerSeconds) |
object |
| 401 | Authentication token not valid | |
| 404 | No slide currently active |
/slides/current/layers
GET
Description
Returns the media layers of the currently active slide as Media layer objects.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Media layer documents | object |
| 401 | Authentication token not valid | |
| 404 | No slide currently active |
/slides/save
POST
Description
Saves the currently active slide, capturing its current media layers and controls.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 403 | Saving slides is disabled via X-Manager |
| 404 | No slide currently active |
/slides/{id}/rename
POST
Description
Renames the slide with the given ID. The new name is sent as a JSON body: { "name": "New name" }.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 400 | Missing name in request body |
| 401 | Authentication token not valid |
| 404 | No slide with specified ID found in the current experience |
/slides/{id}/timer
POST
Description
Sets how long the slide with the given ID is shown before auto-forward moves on. The duration is sent as whole seconds in a JSON body: { "seconds": 30 }. Zero removes the timer, which is what DELETE does. There is no upper bound. Setting the timer of the currently active slide restarts its countdown from the new duration.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 400 | seconds is negative |
| 401 | Authentication token not valid |
| 404 | No slide with specified ID found in the current experience |
DELETE
Description
Removes the timer from the slide with the given ID, so it is no longer shown for a duration of its own. The auto-forward delay still applies to it afterwards.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 404 | No slide with specified ID found in the current experience |
/auto-forward
GET
Description
Returns the auto-forward countdown of the current experience.
The countdown does not tick over the network — it is only reported when it starts, stops, is paused or resumed. A caller wanting a live readout counts remainingMs down against its own clock from the moment it read this, exactly as the app's own timer ring does.
| 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 this was read. |
delaySeconds |
number | The auto-forward delay applied to slides without a timer of their own, 0 when auto-forward is off. Fractional when it comes from a client configuration that is not a whole number of seconds. |
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Auto-forward document | object |
| 401 | Authentication token not valid |
/auto-forward/delay
POST
Description
Sets the auto-forward delay of the current experience, which applies to every slide that carries no timer of its own. The delay is sent as whole seconds in a JSON body: { "seconds": 10 }. Zero switches auto-forward off — there is no separate enable/disable and no remembered previous value. There is no upper bound.
The delay set here overrides the one 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. A countdown already running for a slide without its own timer restarts from the new delay.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 400 | seconds is negative |
| 401 | Authentication token not valid |
| 404 | Not in an experience |
/auto-forward/pause
POST
Description
Pauses the running countdown. Nobody owns the pause: the app's own timer ring resumes it just as readily as /auto-forward/resume does. Calling it on an already-paused countdown 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 the countdown from resuming when freeze is lifted.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 409 | No auto-forward countdown is running |
/auto-forward/resume
POST
Description
Resumes the paused countdown, which carries on from where it stopped rather than starting over. Calling it on an already-running countdown succeeds and changes nothing.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 409 | No auto-forward countdown is running, or the presentation is frozen |
/media
GET
Description
Returns every media item known to the currently joined experience, independent of slide or layer placement.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Media documents (mediaId, mediaType, name) |
object |
| 401 | Authentication token not valid |
/media/{mediaId}/layer
GET
Description
Creates a new media layer for the given media on the currently active slide, positioned near the center of the visible canvas, and returns the created Media layer object.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Media layer document | object |
| 401 | Authentication token not valid | |
| 404 | No media with specified ID found, or no slide currently active |
/layers/{id}
GET
Description
Returns the media layer with the given ID on the currently active slide as a Media layer object, with its current control state nested under control. Use this to refresh a single layer — including a playing video's position — without fetching the whole slide.
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Media layer document | object |
| 401 | Authentication token not valid | |
| 404 | No slide currently active, or no media layer with specified ID found |
PATCH
Description
Updates individual properties of the media layer with the given ID on the currently active slide. The request body is a JSON object holding any subset of the writable Media layer object fields; only the fields present are applied, the rest stay untouched. Writable fields are position, size, rotation, crop, visible, alpha, zOrder, locked, fullscreen, sectionFullscreen, backgroundColor, isStatic and layerGroup. position, size and crop are sent whole (all their components). fullscreen and sectionFullscreen are mutually exclusive — setting one clears the other.
Request body sample
{
"position": { "x": 0.25, "y": 0.25 },
"size": { "x": 0.5, "y": 0.28 },
"rotation": 90,
"visible": true,
"alpha": 0.8
}
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 400 | Request body is empty, contains no known layer properties, or layerGroup is a system-assigned or unknown group |
| 401 | Authentication token not valid |
| 404 | No slide currently active, or no media layer with specified ID found |
DELETE
Description
Deletes the media layer with the given ID from the currently active slide, including its control state.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 401 | Authentication token not valid |
| 404 | No slide currently active, or no media layer with specified ID found |
/shortcuts/{name}
GET
Description
Returns the current state of a shortcut that has readable state. {name} is matched case-insensitively. Only ForceWindowFocus supports this — MouseCursor and ShowWelcomeScreen are pure toggles with nothing to read back.
Response body sample
{ "enabled": true }
Responses
| Code | Description | Schema |
|---|---|---|
| 200 | Current state | object |
| 400 | {name} is not ForceWindowFocus |
|
| 401 | Authentication token not valid | |
| 404 | The force-focus watcher isn't active on this instance (editor, non-Windows, or server role) |
POST
Description
Fires an app-side shortcut, exactly as pressing its key combination on the X-Creator would. {name} is matched case-insensitively. Only these three shortcuts are exposed:
{name} |
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 endpoint can only flip them. ForceWindowFocus is the exception — read its resulting state back with GET /shortcuts/ForceWindowFocus above. No experience needs to be joined.
Responses
| Code | Description |
|---|---|
| 200 | Successful response |
| 400 | Unknown shortcut name, or a shortcut that cannot be triggered remotely |
| 401 | Authentication token not valid |
Media Control Endpoints
All routes below are POST /v1/layers/{id}/<path>, where {id} is a mediaLayerId, and return 200 on success with no body, or the same 400/401/404 codes as other layer routes (400 when the layer's mediaType doesn't support the operation, 404 when the layer doesn't exist). To read the resulting state back, use GET /layers/{id}.
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.
Volume and mute — the video/volume, audio/volume, and ndi/volume operations 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)
| Path | Body | Description |
|---|---|---|
video/play |
{ "isPlaying": bool } |
Play or pause at the media's current position; never seeks. |
video/seek |
{ "position": number } |
Seek without changing play state. 0–1, normalized over the media's duration. |
video/mode |
{ "playMode": string } |
Loop, ResetAfterPlay, PlayToEnd, or ForwardSlideWhenFinished. |
video/volume |
{ "volume": number } |
0–1. Also unmutes, or mutes at 0. |
video/mute |
{ "mute": bool } |
Audio (all Layer-scoped)
| Path | Body | Description |
|---|---|---|
audio/play |
{ "play": bool } |
Play or pause at the media's current position; never seeks. |
audio/position |
{ "position": number } |
Seek. 0–1, normalized over the media's duration. |
audio/loop |
{ "loop": bool } |
|
audio/volume |
{ "volume": number } |
0–1. Also unmutes, or mutes at 0. |
audio/mute |
{ "mute": bool } |
Pdf (all Layer-scoped)
| Path | Body | Description |
|---|---|---|
pdf/page |
{ "page": int } |
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 |
{ "interaction": bool } |
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)
| Path | Body | Description |
|---|---|---|
web/url |
{ "url": string } |
Loads a new URL. |
web/navigate |
{ "direction": string } |
Back, Forward, or Reload. |
Ndi (all Media-scoped)
| Path | Body | Description |
|---|---|---|
ndi/volume |
{ "volume": number } |
0–1. Also unmutes, or mutes at 0. |
ndi/mute |
{ "mute": bool } |
|
ndi/color-format |
{ "colorFormat": string } |
BGRX_BGRA, UYVY_BGRA, RGBX_RGBA, UYVY_RGBA, or Fastest. |
ndi/bandwidth |
{ "bandwidth": string } |
Highest, Lowest, or AudioOnly. |
Text
| Path | Scope | Body | Description |
|---|---|---|---|
text/text |
Media | { "text": string } |
|
text/font |
Layer | { "fontName": string } |
|
text/font-size |
Layer | { "fontSize": number } |
|
text/font-color |
Layer | { "fontColor": { "r", "g", "b", "a" } } |
|
text/alignment |
Layer | { "alignment": string } |
Unity TextAnchor member, e.g. UpperLeft, MiddleCenter. |
text/interaction |
Layer | { "interaction": bool } |
Shape (Layer-scoped)
| Path | Body | Description |
|---|---|---|
shape/color |
{ "color": { "r", "g", "b", "a" } } |
Not exposed: freeze (any media type), NDI PTZ (ndi/ptz/*) and camera presets, Web scale/interaction/interaction-authority, and AssetBundle/VideoMatrix controls entirely.
Samples
Load next slide:http://192.168.0.1:9696/api/v1/slides/next
Request all experiences:http://192.168.0.1:9696/api/v1/experiences
Request the media layers of the current slide:http://192.168.0.1:9696/api/v1/slides/current/layers
Update a media layer's opacity (PATCH with a JSON body { "alpha": 0.5 }):http://192.168.0.1:9696/api/v1/layers/<layerId>
Tools
Built-in Test Console
Opening http://<ip>:<port>/ (the same host and port as the API) in a browser serves a built-in interactive console for sending requests and inspecting responses without any external tooling.
Troubleshooting
Response Code 400 - Bad Request
If a required field is missing from the request (e.g. name when renaming a slide), the status code "400 Bad Request" is returned.
Response Code 401 - Unauthorized
If Authentication is enabled and the request is missing a valid Authorization: Bearer <token> header, the status code "401 Unauthorized" is returned.
Response Code 404 - Not Found
If a non-existent endpoint is requested, or the requested experience/slide does not exist (or is not currently loaded), the status code "404 Not Found" is returned.
Response Code 403 - Forbidden
If saving slides has been disabled via X-Manager, the status code "403 Forbidden" is returned.
Response Code 409 - Conflict
If the requested slide navigation cannot be performed in the current situation (e.g. no next/previous slide available, or attempting to delete the last remaining slide), the status code "409 Conflict" is returned.
Slide navigation is a request
Slide navigation (/slides/next, /slides/previous, /slides/{id}/activate) asks the server of the shared experience to change the active slide, so that all clients of that experience show the same slide. A "200" response confirms that the request was accepted and passed on, not that the slide has already changed.
/slides/next and /slides/previous calls that arrive close together are collected into a single move covering all of them — moving three slides ahead by calling /slides/next three times in quick succession loads only the slide arrived at, not each one passed on the way. /slides/{id}/activate is never collected like this, since it already names the exact slide it wants.
The server can still decide not to carry a request out, for example when it is superseded by another activation before it is applied. No slide change happens then, and the response is still "200". Read /slides/current to find out which slide is active now.
Response Code 500 - Internal Server Error
If this error occurs please contact the administrator. The status code "500 Internal Server Error" is returned.
Response Code 501 - Not Implemented
Returned by endpoints that require this X-Creator to be running the Host role (e.g. looking up slides of an experience without joining it) when it isn't.