The SimDeck server exposes one REST API over plain HTTP. Every route lives under /api/. Responses are JSON unless explicitly noted otherwise. Errors return a JSON body with {"error": "..."} and an appropriate HTTP status.
The served browser UI receives the generated access token automatically through a strict same-site cookie. Direct API callers must send X-SimDeck-Token: <token> or Authorization: Bearer <token>.
- Method casing follows REST conventions.
GETfor queries,POSTfor state changes. - Path parameters use
{name}notation in this reference. UDIDs come fromGET /api/simulators(orsimdeck list). - Most mutation endpoints return
{ "ok": true }; boot and shutdown return refreshed simulator metadata. - Timestamps are numeric unless a route documents otherwise.
Returns server health and the active video encoder mode.
{
"ok": true,
"httpPort": 4310,
"timestamp": 1714094761.234,
"videoCodec": "auto",
"lowLatency": false,
"webRtc": {
"iceServers": [{ "urls": ["stun:stun.l.google.com:19302"] }],
"iceTransportPolicy": "all"
}
}The browser client polls this endpoint at startup to detect server restarts and to mirror the daemon's WebRTC ICE configuration.
Returns server-side video stats, active encoder overload states, and a rolling buffer of client-side stats. See Video Pipeline for an annotated example.
Returns just the client-side stats:
{ "clientStreams": [{ "clientId": "...", "kind": "viewport", ... }] }Submit a stats sample from a client. The server keeps the last 48 entries per (clientId, kind):
POST /api/client-stream-stats
Content-Type: application/json
{
"clientId": "browser-ABC",
"kind": "viewport",
"codec": "h264",
"width": 1170,
"height": 2532,
"decodedFps": 59.7,
"droppedFps": 0.0,
"latestRenderMs": 6.2
}Required fields: clientId and kind. Every other field is optional but typed in ClientStreamStats.
Returns the active stream encoder settings and available quality profiles.
Updates the active stream encoder settings for newly encoded frames. The browser UI uses this before WebRTC negotiation when the user selects encoder, FPS, or quality.
{
"videoCodec": "hardware",
"fps": 120,
"profile": "quality"
}videoCodec accepts hardware or software from the UI, and the API also
accepts auto. fps is clamped to the local stream range. Browser viewers show
five profiles: quality (4096 px), balanced (1280 px), economy (1080 px),
low (720 px), and tiny (540 px). The API still accepts the legacy fast,
smooth, and ci-software profiles for CLI/provider compatibility. When
profile is provided, its resolution preset is applied; send maxEdge without
profile for a custom resolution cap.
Returns every simulator known to the native bridge, enriched with any session state SimDeck has attached:
{
"simulators": [
{
"udid": "9D7E5BB7-...",
"name": "iPhone 15 Pro",
"runtimeName": "iOS 18.0",
"deviceTypeIdentifier": "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro",
"isBooted": true,
"privateDisplay": {
"displayReady": true,
"displayStatus": "running",
"displayWidth": 1170,
"displayHeight": 2532,
"frameSequence": 8124,
"rotationQuarterTurns": 0
}
}
]
}privateDisplay is null until a stream attaches.
Boots the simulator and returns the refreshed simulator metadata:
{ "simulator": { ... } }Tears down the live session (if any) and shuts the simulator down.
Toggles between light and dark appearance via simctl ui appearance.
{ "ok": true }Forces the encoder to emit a fresh keyframe. Useful after a discontinuity or when the client decoder drifts.
{ "ok": true }WebRTC transport for browser-native live video. The browser sends an SDP offer and the server responds with an SDP answer for a receive-only H.264 video track:
{
"sdp": "v=0\r\n...",
"type": "offer"
}{
"sdp": "v=0\r\n...",
"type": "answer"
}The endpoint requires the active simulator stream to produce H.264-compatible samples. The bundled browser client always uses this endpoint.
The browser also opens a simdeck-control data channel. In addition to input
messages, clients can tune the stream attached to that peer:
{ "type": "streamControl", "profile": "thumb" }Supported profiles are thumb/thumbnail, focus/full, and paused.
Clients may also send fps, forceKeyframe, or snapshot fields.
Opens a URL inside the simulator:
POST /api/simulators/{udid}/open-url
Content-Type: application/json
{ "url": "https://example.com" }{ "ok": true }Launches an installed app:
POST /api/simulators/{udid}/launch
Content-Type: application/json
{ "bundleId": "com.apple.Preferences" }{ "ok": true }Replays a single touch event. For drags, send began, one or more moved, then ended (or cancelled).
POST /api/simulators/{udid}/touch
Content-Type: application/json
{ "x": 240.0, "y": 480.0, "phase": "began" }Allowed phase values: began, moved, ended, cancelled.
Replays multiple normalized touch events through one native input session:
POST /api/simulators/{udid}/touch-sequence
Content-Type: application/json
{
"events": [
{ "x": 0.5, "y": 0.7, "phase": "began", "delayMsAfter": 25 },
{ "x": 0.5, "y": 0.4, "phase": "moved", "delayMsAfter": 25 },
{ "x": 0.5, "y": 0.2, "phase": "ended" }
]
}This is the preferred API for agent gestures because it avoids one HTTP request per touch phase.
Replays a single keyboard event by HID key code:
POST /api/simulators/{udid}/key
Content-Type: application/json
{ "keyCode": 4, "modifiers": 0 }keyCode is the HID usage value. modifiers is a bitmask defined by the HID input subsystem (defaults to 0).
Replays multiple HID key codes through one native input session:
POST /api/simulators/{udid}/key-sequence
Content-Type: application/json
{ "keyCodes": [11, 8, 15, 15, 18], "delayMs": 5 }delayMs defaults to 0.
Presses a hardware button:
POST /api/simulators/{udid}/button
Content-Type: application/json
{ "button": "lock", "durationMs": 50 }Supported button names match the CLI: home, lock, side-button, siri,
and apple-pay. durationMs defaults to 0.
Presses the home button:
{ "ok": true }Invokes the app switcher as one server-side native action.
Rotates the simulator 90° counter-clockwise.
Rotates the simulator 90° clockwise.
Returns the bezel layout for the simulator:
{
"totalWidth": 1240,
"totalHeight": 2602,
"screenX": 35,
"screenY": 35,
"screenWidth": 1170,
"screenHeight": 2532,
"cornerRadius": 220
}The browser client uses this to compose chrome around the live frame.
Returns the rendered bezel as a PNG. Cache headers are set to no-cache, no-store, must-revalidate so changes (e.g. after a device rotation) are picked up immediately.
Returns the current accessibility tree. The server merges framework inspectors, the Swift in-app agent, and the native accessibility tree. Query parameters:
source |
Behaviour |
|---|---|
auto (default) / unset |
Use the most accurate source available, falling back to AX. |
nativescript / ns |
Force the NativeScript logical tree if a NativeScript inspector is connected for the foreground app. |
react-native / rn |
Force the React Native component tree if a React Native inspector is connected for the foreground app. |
swiftui / swift-ui |
Force the published SwiftUI logical tree if the Swift agent root publisher is installed in the app. |
uikit / in-app-inspector |
Force the raw UIKit hierarchy from the in-app inspector agent (NativeScript or Swift). |
native-ax / ax |
Always use the native accessibility snapshot. |
| Parameter | Default | Description |
|---|---|---|
maxDepth |
80 |
Limits returned descendants for in-app inspectors. Native AX responses are trimmed server-side. |
includeHidden |
false |
Includes hidden in-app inspector views when supported by the connected inspector runtime. |
The response always includes:
{
"roots": [...],
"source": "nativescript|react-native|swiftui|in-app-inspector|native-ax",
"availableSources": ["nativescript", "react-native", "swiftui", "in-app-inspector", "native-ax"],
"fallbackReason": "...",
"inspector": { ... }
}fallbackReason is only present when the server could not honour the requested source.
Returns the AX-style accessibility description of the topmost element at a screen point. x and y are in UIKit screen points and must be finite, non-negative numbers.
Returns compact accessibility matches for a selector:
{
"selector": {
"text": "Continue",
"id": "continue-button",
"elementType": "Button",
"enabled": true,
"regex": false
},
"source": "auto",
"maxDepth": 8,
"limit": 20
}Selectors can match text, id, label, value, elementType, index, enabled, checked, focused, and selected. Set regex: true to use regular expression matching for string fields.
Polls until a selector appears. assert is an alias with the same payload shape:
{
"selector": { "text": "Welcome", "regex": true },
"timeoutMs": 5000,
"pollMs": 100
}Use POST /api/simulators/{udid}/wait-for-not or /assert-not for negative assertions.
Scrolls and polls until the selector appears:
{
"selector": { "text": "Settings" },
"direction": "down",
"timeoutMs": 10000
}direction accepts up, down, left, and right.
Proxies a single inspector method to the active in-app inspector (NativeScript or Swift) for the simulator. This is used by the browser client to fetch view properties, list available actions, and run debug-only edits.
POST /api/simulators/{udid}/inspector/request
Content-Type: application/json
{
"method": "View.getProperties",
"params": { "id": "view:0x1234" }
}Allowed methods (the server enforces this allow-list):
Runtime.pingView.getView.evaluateScriptView.getPropertiesView.setPropertyView.listActionsView.perform
The response includes both the inspector's result and metadata about the inspector that handled the request:
{
"result": { "id": "view:0x1234", "properties": [...] },
"inspector": {
"bundleIdentifier": "com.example.MyApp",
"bundleName": "MyApp",
"transport": "websocket",
"processIdentifier": 73214,
"host": "127.0.0.1",
"port": null,
"displayScale": 3,
"protocolVersion": "0.1"
}
}For the full method semantics, see the Inspector Protocol.
Upgrades to a WebSocket. Used by the @nativescript/simdeck-inspector runtime to register itself as an in-app inspector.
After connection the server sends Inspector.getInfo and waits for a response that includes a processIdentifier. Once registered, the server uses this socket as the preferred transport for accessibility-tree and inspector/request calls that target the same process.
Long-poll fallback for environments where the WebSocket transport is not viable. Returns the next pending request as JSON, or 204 No Content after 25 seconds with no work.
Posts a response to a previous polled request:
POST /api/inspector/response
Content-Type: application/json
{
"processIdentifier": 73214,
"id": 12,
"result": { "ok": true }
}Pass error instead of result to deliver an error.
Returns recent simulator logs. Without backfill=true, the server tails the live os_log stream it has already started for the simulator. With backfill=true, the server runs a fresh simctl spawn ... log show over the requested window.
| Query parameter | Default | Notes |
|---|---|---|
backfill |
false |
When true, fetch a one-shot history instead of streaming. |
seconds |
30 |
Backfill window in seconds. Clamped to [1, 1800]. |
limit |
250 |
Max entries to return. Clamped to [1, 1000]. |
levels |
none | Comma-separated list of log levels to keep (debug,info,notice,error,fault). |
processes |
none | Comma-separated list of process names (case-insensitive substring matches). |
q |
none | Free-text filter applied to the rendered log message. |
{
"entries": [
{
"timestamp": "2026-04-23T19:14:12.123Z",
"level": "info",
"process": "MyApp",
"subsystem": "com.example.MyApp",
"category": "ui",
"pid": 73214,
"message": "Loaded 12 items"
}
]
}Error bodies look like:
{
"error": {
"message": "Unknown simulator 9D7E5BB7-..."
}
}| Status | Cause |
|---|---|
400 |
Bad request body or query parameter (e.g. missing url, invalid x/y). |
404 |
Unknown simulator. |
408 |
Timed out waiting for a downstream component (encoder keyframe, AX, inspector). |
500 |
Unhandled native bridge error. Always reported as JSON with the original message preserved. |