Complete technical reference for all REST API endpoints used by the PepperDash Essentials Web Config App.
All API endpoints are accessed through the base path /cws/:appId/api where :appId is the program slot identifier (e.g. app01 through app10).
Base URL: https://[processor-ip]/cws/:appId/api
Protocol: HTTPS only
Authentication: Credential-based via POST /loginCredentials (see below)
Content-Type: application/json for POST requests
Response Format: JSON
Purpose: Authenticate with the processor. The backend uses a single shared authentication mechanism for all program slots.
POST /loginCredentialsRequest Body:
{
"username": "admin",
"password": "yourpassword"
}Response: 200 OK (empty body) on success
Notes:
- A successful response with any
appIdauthenticates the session for all running slots - The app probes all 10 slots in parallel after initial auth to discover which are running
- A
4xxor network error indicates invalid credentials or that the slot is not running
Purpose: Retrieve all loaded assemblies and their versions
GET /versionsResponse:
[
{
"Name": "PepperDash-Essentials",
"Version": "1.8.0.0"
},
{
"Name": "System.Core",
"Version": "4.2.1.0"
}
]Response Fields:
Name(string): Full assembly nameVersion(string): Version number in dotted format
Usage: Displayed on Versions page for system documentation and troubleshooting
Error Conditions:
500: Server error if version information cannot be retrieved
Purpose: Retrieve all available REST API routes registered on the processor
GET /apiPathsResponse:
{
"url": "https://192.168.1.100/cws/app01",
"routes": [
{
"Name": "getDevices",
"Url": "app01/api/devices",
"DataTokens": { "Name": "getDevices" },
"RouteHandler": null
}
]
}Response Fields:
url(string): Base URL of the processor web server for this app slotroutes(array): List of route objectsName(string): Route nameUrl(string): Route URL relative to the baseDataTokens.Name(string): Data token name when present
Usage: Displayed on the API Paths page; routes are sorted alphabetically and shown with clickable URLs
Error Conditions:
500: Server error if route information cannot be retrieved
Purpose: Retrieve all available device types supported by current plugins
GET /typesResponse:
[
{
"Type": "samsungMDC",
"Description": "Samsung displays using MDC protocol",
"CType": "PepperDash.Essentials.Devices.Displays.SamsungMDCDisplay"
},
{
"Type": "basicTriList",
"Description": "Basic TriList device for control systems",
"CType": "PepperDash.Essentials.Core.Devices.BasicTriList"
}
]Response Fields:
Type(string): Configuration identifier used in device configurationDescription(string): Human-readable description of device purposeCType(string): Full .NET class name that implements the device
Usage: Displayed on Types page for configuration reference and development
Error Conditions:
500: Server error if type information cannot be retrieved
Purpose: Retrieve all configured devices in the system
GET /devicesResponse:
[
{
"Key": "Display-Room1",
"Name": "Conference Room Display"
},
{
"Key": "Codec-Main",
"Name": "Main Video Codec"
}
]Response Fields:
Key(string): Unique device identifier used in configuration and debug messagesName(string): Human-readable device name for user interfaces
Usage: Device list display and debug message filtering
Error Conditions:
500: Server error if device information cannot be retrieved
Purpose: Retrieve current properties and values for a specific device
GET /deviceProperties/{deviceKey}Path Parameters:
deviceKey(string): The unique Key of the device
Response:
[
{
"Name": "PowerIsOn",
"Type": "Boolean",
"Value": "true",
"CanRead": true,
"CanWrite": true
},
{
"Name": "CurrentInput",
"Type": "String",
"Value": "HDMI1",
"CanRead": true,
"CanWrite": false
}
]Response Fields:
Name(string): Property nameType(string): Data type of the property valueValue(string): Current property value (always string representation)CanRead(boolean): Whether property value can be readCanWrite(boolean): Whether property value can be modified
Usage: Device detail inspection and monitoring
Error Conditions:
404: Device with specified key not found500: Server error retrieving device properties
Purpose: Retrieve available methods (commands) for a specific device
GET /deviceMethods/{deviceKey}Path Parameters:
deviceKey(string): The unique Key of the device
Response:
[
{
"Name": "PowerOn",
"Params": []
},
{
"Name": "SetInput",
"Params": [
{
"Name": "input",
"Type": "String"
}
]
}
]Response Fields:
Name(string): Method nameParams(array): Array of parameter definitionsName(string): Parameter nameType(string): Parameter data type
Usage: Device control interface and method execution
Error Conditions:
404: Device with specified key not found500: Server error retrieving device methods
Purpose: Retrieve the complete merged system configuration
GET /configResponse: Complete JSON configuration object (structure varies by system)
Example Response Structure:
{
"devices": {
"Display-Room1": {
"key": "Display-Room1",
"name": "Conference Room Display",
"type": "samsungMDC",
"properties": {
"control": {
"tcpSshProperties": {
"address": "192.168.1.100",
"port": 1515
}
}
}
}
},
"rooms": {
"ConferenceRoom": {
"name": "Conference Room",
"devices": ["Display-Room1"]
}
}
}Usage: Configuration analysis, backup, and documentation
Response Size: Can be very large (10KB - 1MB+) depending on system complexity
Error Conditions:
500: Server error if configuration cannot be retrieved or merged
Purpose: Initiate a WebSocket debug session for real-time message monitoring
GET /debugSessionResponse:
{
"url": "wss://192.168.1.100/cws/app01/api/debug-websocket"
}Response Fields:
url(string): WebSocket URL for establishing debug connection
Usage: Real-time debug message monitoring in Debug Console
WebSocket Protocol:
- Connection: Use returned URL to establish WebSocket connection
- Messages: Server sends JSON-formatted log messages
- Client: Client receives messages, no need to send data to server
WebSocket Message Format:
{
"Timestamp": "2024-01-15T10:30:45.123Z",
"MessageTemplate": "Device {Key} power state changed to {State}",
"RenderedMessage": "Device Display-Room1 power state changed to On",
"Level": "Information",
"Properties": {
"Key": "Display-Room1",
"State": "On"
}
}Error Conditions:
500: Server error if debug session cannot be started- WebSocket connection errors handled by client WebSocket implementation
Purpose: Stop an active debug session and close WebSocket connections
POST /debugSessionRequest Body: None required
Response: Empty (204 No Content on success)
Usage: Clean termination of debug sessions
Error Conditions:
500: Server error stopping debug session
Purpose: Retrieve the current minimum log level for debug output
GET /appdebugResponse:
{
"minimumLevel": "Information"
}Response Fields:
minimumLevel(string): Current minimum log level setting
Valid Log Levels (in order of severity):
Verbose: Most detailed loggingDebug: Detailed technical informationInformation: General informational messagesWarning: Warning conditionsError: Error conditionsFatal: Fatal error conditions
Usage: Display current log level setting and provide options for changes
Error Conditions:
500: Server error retrieving log level setting
Purpose: Change the minimum log level for debug output
POST /appdebugRequest Body:
{
"minimumLevel": "Warning"
}Request Fields:
minimumLevel(string): New minimum log level (must be valid level)
Response: Empty (204 No Content on success)
Usage: Adjust debug verbosity from Debug Console interface
Error Conditions:
400: Invalid log level specified500: Server error setting log level
Purpose: Check if configuration loading on boot is disabled
GET /doNotLoadConfigOnNextBootResponse:
{
"doNotLoadConfigOnNextBoot": false
}Response Fields:
doNotLoadConfigOnNextBoot(boolean): Whether config loading is disabled
Usage: Display current setting and allow user control
Error Conditions:
500: Server error retrieving setting
Purpose: Enable or disable configuration loading on next boot
POST /doNotLoadConfigOnNextBootRequest Body:
{
"doNotLoadConfigOnNextBoot": true
}Request Fields:
doNotLoadConfigOnNextBoot(boolean): New setting value
Response: Empty (204 No Content on success)
Usage: Control configuration loading behavior for troubleshooting
Error Conditions:
400: Invalid boolean value specified500: Server error setting configuration
Purpose: Restart the entire PepperDash Essentials framework
POST /restartProgramRequest Body: None required
Response: Empty (may not receive response due to restart)
Usage: Complete system restart from web interface
Behavior:
- System begins restart immediately
- All connections will be closed
- Response may not be received due to restart timing
- System will be unavailable for 2-5 minutes during restart
Error Conditions:
500: Server error initiating restart (system may still restart)
Purpose: Manually reload system configuration without full restart
POST /loadConfigRequest Body: None required
Response: Empty (204 No Content on success)
Usage: Apply configuration changes without full system restart
Prerequisites: Usually used when "doNotLoadConfigOnNextBoot" is true
Error Conditions:
400: Configuration cannot be loaded (syntax errors, etc.)500: Server error during configuration loading
Requires PepperDashEssentials.dll 3.0 or later.
Purpose: Retrieve the complete routing graph — devices, ports, tie lines, and current route state
GET /routingDevicesAndTieLinesResponse:
{
"devices": [
{
"key": "display-1",
"name": "Main Display",
"hasInputs": true,
"hasOutputs": false,
"hasInputsAndOutputs": false,
"inputPorts": [
{
"key": "hdmiIn1",
"signalType": "AudioVideo",
"connectionType": "Hdmi",
"isInternal": false
}
]
}
],
"tieLines": [
{
"sourceDeviceKey": "laptop-1",
"sourcePortKey": "out1",
"destinationDeviceKey": "display-1",
"destinationPortKey": "hdmiIn1",
"signalType": "AudioVideo",
"isInternal": false
}
],
"currentRoutes": [],
"sinkCurrentSources": [],
"multiviewLayouts": {}
}Device role flags: hasInputsAndOutputs is the literal "is a midpoint" flag. Note that a multiview decoder reports hasInputs and hasOutputs while not being a midpoint, so "sink = has inputs and no outputs" is not a safe test — see UI Components Reference.
Multiview tiles: A multiview decoder's tile child devices are not returned as top-level devices. Each tile is synthesized as an input port on the parent, keyed tile{N}:{portKey}, and any tie line or route targeting a tile is remapped onto the parent.
Purpose: Start the routing feedback WebSocket server and obtain its URL
GET /routingFeedbackSessionResponse:
{
"url": "wss://192.168.1.164:65401/routing/join/",
"fallbackUrl": "wss://10.0.0.5:65401/routing/join/"
}Usage: Connect to url to receive live route changes. Messages are a discriminated union on type: snapshot, midpointRouteChanged, sinkInputChanged, and layoutChanged. A sinkInputChanged with an empty sourceDeviceKey means the route feeding that input was cleared.
Purpose: Make or clear a route, addressed entirely by device and port keys
POST /routingCommandPorts are addressed by key, never by selector: a routing port's selector is a driver-defined object that cannot be expressed in JSON, so the processor resolves key → port → selector itself.
Request Body — one of four commands:
{ "command": "sinkRoute", "deviceKey": "display-1", "inputPortKey": "hdmiIn1",
"sourceDeviceKey": "laptop-1", "signalType": "AudioVideo" }
{ "command": "midpointSwitch", "deviceKey": "dm-chassis-1",
"inputPortKey": "inputCard3", "outputPortKey": "outputCard5", "signalType": "Video" }
{ "command": "clearSink", "deviceKey": "display-1", "inputPortKey": "hdmiIn1",
"clearSinkInput": true }
{ "command": "clearMidpointOutput", "deviceKey": "dm-chassis-1",
"outputPortKey": "outputCard5", "signalType": "AudioVideo" }Request Fields:
command(string):sinkRoute,midpointSwitch,clearSinkorclearMidpointOutput. Case-insensitivedeviceKey(string): Target device. For sink commands this is the destination — or the multiview parent when addressing a tileinputPortKey(string, optional): May be multiview-qualified (tile2:tileInput); the processor de-qualifies it to the child tile sink. Optional forclearSink, where omitting it clears whatever route the sink hasoutputPortKey(string): Required for the midpoint commandssourceDeviceKey(string): Required forsinkRoutesourcePortKey(string, optional): Omit to let the processor's path discovery choosesignalType(string):Audio,Video,AudioVideoorUsb. Defaults toAudioVideo. Numeric values are rejectedreleaseOnly(bool):clearSinkonly — stop usage tracking but leave the signal flowingclearSinkInput(bool):clearSinkonly — also deselect the destination's own input. Off by default, because clearing a route otherwise never touches the destinationdryRun(bool): Validate and compute the path, execute nothing
Response:
{
"status": "accepted",
"command": "sinkRoute",
"deviceKey": "nvx-decoder-1",
"resolvedDeviceKey": "nvx-decoder-1-tile2",
"resolvedInputPortKey": "tileInput",
"signalType": "AudioVideo",
"effectiveSignalType": "AudioVideo",
"partial": false,
"steps": [
{
"signalType": "Video",
"switchingDeviceKey": "dm-chassis-1",
"inputPortKey": "inputCard3",
"outputPortKey": "outputCard5"
}
]
}Response Fields:
status:executed(done before the response was written),accepted(validated and queued),validated(dry run), orerrorresolvedDeviceKey/resolvedInputPortKey: The real device and port the command ran against. These differ from the requested values only when atile{N}:port was de-qualifiedeffectiveSignalType: What was actually handed to the devices. May be wider thansignalType— a pre-mapped route descriptor takes its type from the port's declared type, so an Audio-only request across all-AudioVideoports executes asAudioVideorather than breaking awaysteps: The switch steps that will run, in order.sinkRouteonly; anAudioVideoroute is discovered as two independent paths, so each step names its own signal typepartial: AnAudioVideorequest that found a path for only one half. The half that was found is still routed
Status Codes:
| Status | Meaning |
|---|---|
200 |
midpointSwitch / clearMidpointOutput executed inline, or a dry run validated |
202 |
sinkRoute / clearSink validated and queued. Confirmation arrives over the feedback WebSocket, not here |
400 |
Malformed request: invalidJson, missingField, unknownCommand, invalidSignalType |
404 |
deviceNotFound — the key is wrong |
409 |
noRouteFound — the keys are valid but no wiring path exists |
422 |
deviceNotRoutable, tileNotFound, portNotFound, signalTypeNotSupportedByPort — the keys exist but the request is impossible on that device |
500 |
executionError — the device threw while switching |
Error Response Body:
{
"status": "error",
"error": {
"code": "noRouteFound",
"message": "No Video path exists from 'laptop-1' to 'display-1' input 'hdmiIn1'.",
"field": "signalType"
}
}Why sink commands return 202: making a route enqueues onto the processor's single-worker routing queue, and completion is conditional — a destination that is warming or cooling parks its request until the cooldown finishes. The 202 is still meaningful because all validation, including full path discovery, happens synchronously first: a request that provably cannot work returns 409 before anything is queued.
Capability detection: this endpoint does not exist on older processors. Probe Get API Paths for a routingCommand route rather than gating on a version number.
Credential storage. No endpoint here ever returns a stored secret value — the processor is write-only for values by design.
Mutations are POST rather than DELETE: the Crestron web server advertises only POST, GET, OPTIONS,
so a DELETE would fail CORS preflight.
GET /secrets/providers{
"providers": [
{
"key": "default",
"description": "Default secret provider serving Essentials Application 1",
"scope": "local",
"enumerationSupported": true,
"maxKeyLength": 32,
"maxValueLength": 1600
},
{
"key": "CrestronGlobalSecrets",
"description": "Default secret provider serving all local applications",
"scope": "global",
"enumerationSupported": true,
"maxKeyLength": 32,
"maxValueLength": 1600
}
]
}scope is local (this program slot) or global (shared across slots). The limits are Crestron
Data Store caps, not application choices.
GET /secrets?provider=defaultOptional: includeReserved=true to show the API's own bookkeeping records, includeSizes=true to
include each value's character count.
{
"provider": "default",
"scope": "local",
"indexStatus": "ok",
"enumerationComplete": true,
"counts": { "total": 3, "managed": 1, "unmanaged": 2, "stale": 0 },
"secrets": [
{
"key": "displayPassword",
"managed": true,
"description": "Display admin",
"createdUtc": "2026-09-10T14:00:00Z",
"updatedUtc": "2026-09-16T18:22:05Z",
"lastModifiedUtc": "2026-09-16T18:22:05Z",
"owner": "app01",
"type": "String"
}
],
"staleIndexEntries": [],
"warnings": []
}Response Fields:
managed— whether the key was written through this API. Unmanaged records exist in the same flat Data Store but were created elsewhere; Mobile Control's paired-client tokens are oneindexStatus—ok,missing, orcorrupt:<reason>. Anything butokmeans classification is unavailable and every key reports as unmanaged. The secrets themselves are unaffectedenumerationComplete—falsemeans the Data Store walk was cut short and the list is partial. Index pruning is refused in that statestaleIndexEntries— keys the index lists that no longer exist in the store
Status: 200 · 400 missingField · 404 providerNotFound · 500
POST /secrets/command{
"action": "set",
"provider": "default",
"key": "displayPassword",
"value": "…",
"description": "Display admin",
"overwrite": false
}| Action | Semantics |
|---|---|
set |
Creates. Existing key + overwrite:false → 409 alreadyExists |
update |
Must already exist, else 404 notFound |
delete |
Removes the secret and its index entry |
test |
Existence probe → {"exists": true}. Never a value |
pruneIndex |
Drops index entries with no matching record. 409 if the walk was incomplete |
rebuildIndex |
Repairs a damaged index. adoptKeys marks existing keys as managed without reading or changing their values |
{
"status": "ok",
"action": "set",
"provider": "default",
"key": "displayPassword",
"existedBefore": false,
"indexUpdated": true
}indexUpdated: false with a warning means the secret was written but its metadata was not. That is
reported as success, because the secret is authoritative and the index is advisory — returning an
error would invite retrying a write that already happened.
Validation (all before any store access):
| Rule | Code | Status |
|---|---|---|
| Key empty or whitespace | emptyKey |
400 |
| Key over 32 characters, or containing control characters | keyTooLong / invalidKey |
400 |
Key starts with __essSecretsIdx |
reservedKey |
400 |
| Value empty (empty means delete in the store) | emptyValue |
400 |
| Value over 1600 characters | valueTooLong |
400 |
The empty-key rule is not cosmetic: an empty key reaches a Data Store call that deletes every record belonging to the application.
Status: 200 · 400 · 403 accessDenied · 404 notFound · 409 alreadyExists /
enumerationIncomplete · 507 storeFull · 503 storeUnavailable · 500
POST /secrets/bulk{
"mode": "preview",
"provider": "default",
"overwrite": false,
"secrets": { "displayPassword": "…", "codecPassword": "…" }
}secrets accepts either the flat map above — which is what the template endpoint emits — or an
array of {key, value, provider?, description?}.
{
"mode": "preview",
"provider": "default",
"overwrite": false,
"summary": {
"total": 3,
"create": 1,
"overwrite": 1,
"skip": 1,
"invalid": 0,
"failed": 0
},
"entries": [
{
"index": 0,
"key": "displayPassword",
"provider": "default",
"action": "create",
"applied": false
}
],
"indexUpdated": false
}Rules worth knowing:
- Preview writes nothing.
appliedis alwaysfalse, and the response is200even when entries are invalid — a successful preview of a bad file is not a failed request - A commit with any invalid entry returns
422and writes nothing overwritedefaults tofalse- An entry targeting an existing unmanaged key is skipped as
unmanagedTargetunlessallowUnmanagedOverwriteis set. This is what stops a round-tripped template destroying another subsystem's records - Bulk never deletes. A key absent from the file is left alone
- Duplicate keys within one batch are
invalid, not last-write-wins - Over 200 entries →
400 batchTooLarge
GET /secrets/template?provider=default{
"provider": "default",
"generatedUtc": "2026-09-16T18:30:00Z",
"note": "Values are intentionally blank. Fill them in, then apply this file.",
"secrets": { "displayPassword": "", "codecPassword": "" },
"metadata": [
{
"key": "displayPassword",
"provider": "default",
"description": "Display admin",
"managed": true
}
]
}secrets is byte-for-byte what the bulk endpoint accepts, so the file round-trips: download, fill
in, apply elsewhere. includeUnmanaged defaults to false, so other subsystems' records are not
offered as blanks to fill in.
Capability detection: these endpoints do not exist on older processors. Probe
Get API Paths for a secrets route rather than gating on a version number.
All endpoints return errors in consistent format:
HTTP Status Codes:
400: Bad Request - Invalid parameters or request format404: Not Found - Requested resource does not exist500: Internal Server Error - Server-side processing error
Error Response Body:
{
"error": "Description of the error condition",
"details": "Additional technical details (optional)"
}Rate Limits: No explicit rate limiting implemented
Performance Considerations:
/configendpoint may take several seconds for large configurations/debugSessionWebSocket can generate high message volumes- Multiple simultaneous debug sessions impact processor performance
Best Practices:
- Cache version and type information (changes infrequently)
- Close debug sessions when not actively monitoring
- Use appropriate minimum log levels to reduce debug message volume
Authentication: Uses processor's built-in web authentication (if configured)
Authorization: No granular permissions - full access if authenticated
HTTPS: All communication must use HTTPS (HTTP not supported)
CORS: Configured to allow requests from the web application origin
Sensitive Data:
- Configuration may contain IP addresses, usernames, and network topology
- Debug messages may contain sensitive operational information
- No automatic filtering of sensitive information in responses
- Call
GET /debugSessionto get WebSocket URL - Establish WebSocket connection using returned URL
- Connection remains open until explicitly closed or system restart
- Server to Client: Continuous stream of debug messages in JSON format
- Client to Server: No messages required (read-only protocol)
- Keep-alive: WebSocket handles connection keep-alive automatically
- Reconnection: Client must handle reconnection logic if connection drops
- Cleanup: Call
POST /debugSessionto cleanly stop session
- High Volume: Systems may generate >100 messages per second
- Filtering: Use minimum log level to reduce message volume
- Browser Limits: Very high message rates may impact browser performance
fetch('https://192.168.1.100/cws/app01/api/config')
.then((response) => response.json())
.then((config) => {
console.log('System configuration:', config);
});// Get WebSocket URL
fetch('https://192.168.1.100/cws/app01/api/debugSession')
.then((response) => response.json())
.then((data) => {
// Connect to WebSocket
const ws = new WebSocket(data.url);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Debug message:', message);
};
ws.onclose = () => {
console.log('Debug session closed');
};
});fetch('https://192.168.1.100/cws/app01/api/appdebug', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
minimumLevel: 'Warning',
}),
});This reference provides complete technical details for all API interactions. For practical usage examples, see the How-to Guides and Tutorials.