Skip to content

Devices & Inspector

The devices and inspector API powers Ghost’s mobile testing capabilities. These endpoints let you discover connected iOS simulators and Android emulators, capture screenshots, inspect the UI element tree, generate test automation selectors, correlate UI interactions with network traffic, and produce AI-enhanced bug reports — all from Ghost’s desktop interface.

Think of the inspector as a remote control and X-ray machine for your mobile device. You can see what’s on the screen (screenshots), look inside the screen to see the individual UI elements (hierarchy), figure out how to target specific elements in test code (selectors), and connect what the user is doing on screen to what the app is sending over the network (traffic correlation).

GET /api/v1/inspector/devices

Returns all discovered devices — iOS simulators, Android emulators, or physical devices connected via USB or Wi-Fi.

Response:

[
{
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"name": "iPhone 16 Pro",
"platform": "ios",
"os_version": "17.2",
"connection_type": "simulator",
"state": "connected",
"client_ip": "127.0.0.1",
"model": "iPhone16,1",
"running_app": "com.example.MyApp",
"screen_width": 1179,
"screen_height": 2556,
"wda_width": 393,
"wda_height": 852,
"inspector_port": 8100,
"session_id": "01HWXYZ...",
"last_seen": "2024-01-15T10:30:00Z",
"error": ""
}
]
FieldTypeDescription
idstringDevice identifier — UDID for iOS simulators, ADB serial for Android emulators
namestringHuman-readable device name (e.g., “iPhone 16 Pro”, “Pixel 7 API 34”)
platformstring"ios" or "android"
os_versionstringOperating system version
connection_typestringHow the device is connected: "simulator", "emulator", "usb", or "wifi"
statestringCurrent state: "discovered" (found but not connected), "connecting" (connection in progress), "connected" (ready for inspection), "error" (connection failed), "disconnected" (was connected, now stopped)
client_ipstringDevice’s IP address (or localhost for simulators/emulators)
modelstringHardware model identifier
running_appstringBundle ID of the foreground app
screen_width / screen_heightintegerScreen resolution in physical pixels
wda_width / wda_heightintegeriOS only — WDA (WebDriverAgent) reports coordinates in logical points, which differ from physical pixels due to screen scale (e.g., 3x on retina). Ghost uses both coordinate spaces for accurate tap positioning
inspector_portintegerPort of the running inspection agent (WDA for iOS, atx-agent for Android)
session_idstringWDA session ID (iOS only)
last_seentimestampWhen the device was last detected by discovery
errorstringError message if the device is in the "error" state

Returns 503 if the device manager is not initialized.

GET /api/v1/inspector/devices/{id}

Returns a single device by its ID. Same response format as the list endpoint.

Returns 404 if the device is not found.

POST /api/v1/inspector/devices/{id}/connect

Initiates the connection process for a device. This returns immediately with a 202 Accepted response — the actual connection happens in a background goroutine because it can take a while (especially on iOS where WebDriverAgent may need to be built for the first time).

Connection timeout: 120 seconds. WDA build on iOS can take 30–60 seconds on the first run (subsequent connects are much faster because the build is cached).

What happens during connection:

  • Android: Allocates a port (starting from 17912), sets up ADB port forwarding, starts the atx-agent service, and polls until it responds (10 attempts at 500ms intervals)
  • iOS Simulator: Scans ports 8100–8109 for a running WDA instance. If none found, clones the WDA project, builds it, launches it, and waits up to 30 seconds for it to start

Connection progress is broadcast via device.updated and device.connected WebSocket events.

Response: 202 Accepted with the current device info snapshot.

POST /api/v1/inspector/devices/{id}/disconnect

Stops all inspector services for the device — screenshot capture, touch monitoring, and platform-specific cleanup.

Response: {"ok": true}

Broadcasts a device.disconnected WebSocket event.

Ghost captures screenshots from connected devices at approximately 1 frame per second and stores them in a circular ring buffer — like a security camera that keeps the last 30 seconds of footage and overwrites the oldest frame when it runs out of space.

GET /api/v1/inspector/devices/{id}/screenshot?offset=0

Returns a screenshot from the ring buffer as raw binary image data.

Query parameters:

ParameterTypeDefaultDescription
offsetinteger0Which frame to return: 0 = most recent, 1 = one frame before that, up to 29 = oldest available frame

Ring buffer details:

  • Size: 30 frames
  • Capture rate: ~1 frame per second
  • Memory: ~100 KB per frame × 30 frames = ~3 MB per connected device
  • Format: JPEG (Android via atx-agent) or PNG (iOS via simctl)

Response headers:

  • Content-Type: auto-detected from the image data (image/jpeg or image/png)
  • Content-Length: exact byte count
  • Cache-Control: no-store (prevent browser caching — each request should get the latest frame)

Returns 404 if no screenshot is available or the offset exceeds the number of stored frames.

GET /api/v1/inspector/devices/{id}/screenshot/meta

Returns metadata about the ring buffer — used by the frontend to power the scrub slider (a timeline control that lets you scroll through recent screenshots).

Response:

{
"count": 30,
"oldest_ts": 1705312800000,
"newest_ts": 1705312830000
}
FieldTypeDescription
countintegerNumber of frames currently stored (0 to 30)
oldest_tsintegerUnix timestamp in milliseconds of the oldest frame
newest_tsintegerUnix timestamp in milliseconds of the newest frame

Returns all zeros if the buffer is empty.

The element hierarchy is a tree representation of every UI element visible on the screen — buttons, text fields, images, containers, and their nesting relationships. It’s the same data you’d see in Android Studio’s Layout Inspector or Xcode’s View Hierarchy Debugger.

GET /api/v1/inspector/devices/{id}/hierarchy

Returns the complete UI element tree. Android parses the hierarchy from atx-agent’s XML dump. iOS parses from WDA’s source endpoint (depth 15).

Response: A recursive Element tree:

{
"id": "node_0",
"class_name": "android.widget.FrameLayout",
"text": "",
"resource_id": "",
"accessibility_id": "",
"content_desc": "",
"bounds": {"x": 0, "y": 0, "width": 1080, "height": 2400},
"visible": true,
"clickable": false,
"enabled": true,
"scrollable": false,
"depth": 0,
"index": 0,
"platform": "android",
"element_count": 156,
"children": [
{
"id": "node_1",
"class_name": "android.widget.Button",
"text": "Submit",
"resource_id": "com.app:id/submit_button",
"accessibility_id": "Submit",
"content_desc": "Submit form",
"bounds": {"x": 100, "y": 500, "width": 200, "height": 48},
"visible": true,
"clickable": true,
"enabled": true,
"depth": 1,
"index": 0,
"platform": "android",
"children": []
}
]
}

Each Element has these fields:

FieldTypeDescription
idstringUnique identifier within this hierarchy snapshot
class_namestringThe UI component type (e.g., android.widget.Button, XCUIElementTypeButton)
textstringVisible text content
resource_idstringAndroid resource ID (e.g., com.app:id/button)
accessibility_idstringAccessibility identifier (used by screen readers and test automation)
content_descstringAndroid content description
labelstringiOS label property
namestringiOS name property
valuestringiOS value property (e.g., text field content)
boundsobjectScreen position and size: {x, y, width, height}
attributesobjectAdditional platform-specific attributes as key-value pairs
childrenarrayChild elements (the nesting that makes this a tree)
depthintegerHow deeply nested this element is (0 = root)
indexintegerPosition among siblings
platformstring"android" or "ios"
visiblebooleanWhether the element is visible on screen
clickablebooleanWhether tapping this element does something
enabledbooleanWhether the element accepts interaction
focusedbooleanWhether this element currently has focus
scrollablebooleanWhether this element can be scrolled
selectedbooleanWhether this element is in a selected state
checkedbooleanWhether this checkbox/toggle is checked
traitsstringiOS accessibility traits
package_namestringAndroid package name
element_countintegerTotal number of descendants (only on the root element)
GET /api/v1/inspector/devices/{id}/element?x=150&y=300

Finds the deepest (most specific) element at the given screen coordinates. This is what powers the “click on the screenshot to select an element” feature — you click a point on the screenshot, and Ghost figures out which element is at that exact spot by traversing the hierarchy tree.

Query parameters:

  • x (required) — X coordinate in screen pixels
  • y (required) — Y coordinate in screen pixels

Response: A single Element object (same structure as above, but just one element, not the full tree).

Returns 400 if x or y is missing/invalid, 404 if no element is found at that point.

GET /api/v1/inspector/devices/{id}/selectors/{nodeId}

Generates test automation selectors for a specific element — these are the “addresses” you’d use in test code to find this element. Ghost generates multiple selectors using different strategies, ranked by reliability.

Path parameters:

  • {id} — device ID
  • {nodeId} — element ID from the hierarchy tree

Response:

{
"selectors": [
{
"strategy": "accessibility_id",
"value": "Submit",
"reliability": "excellent",
"formats": {
"raw": "Submit",
"appium_java": "driver.findElement(AppiumBy.accessibilityId(\"Submit\"))",
"appium_python": "driver.find_element(AppiumBy.ACCESSIBILITY_ID, \"Submit\")",
"maestro": "- tapOn:\n id: \"Submit\""
}
},
{
"strategy": "resource_id",
"value": "com.app:id/submit_button",
"reliability": "good",
"formats": {
"raw": "com.app:id/submit_button",
"appium_java": "driver.findElement(AppiumBy.id(\"com.app:id/submit_button\"))",
"appium_python": "driver.find_element(AppiumBy.ID, \"com.app:id/submit_button\")",
"espresso": "onView(withId(R.id.submit_button))"
}
}
]
}

Selector strategies by platform:

PlatformStrategies (priority order)Description
Androidaccessibility_id, resource_id, content_desc, ui_selector, xpathaccessibility_id and resource_id are most reliable; xpath is a last resort
iOSaccessibility_id, predicate_string, class_chain, xpathaccessibility_id is best; predicate_string and class_chain are iOS-specific powerful alternatives

Reliability levels:

LevelMeaning
"excellent"Highly stable — unlikely to break across app updates (e.g., accessibility IDs set by developers)
"good"Reliable in most cases (e.g., resource IDs, content descriptions)
"ok"Works but may be fragile (e.g., complex compound selectors)
"fragile"May break easily (e.g., absolute XPath that depends on exact tree structure)

Code format keys:

KeyFramework
rawThe raw selector value without any framework
appium_javaAppium Java client code
appium_pythonAppium Python client code
maestroMaestro YAML format
espressoAndroid Espresso (Android only)
xcuitestXCUITest (iOS only)
GET /api/v1/inspector/devices/{id}/correlation?ts=1705312815000&window=5000

Finds HTTP flows that happened around the same time as a UI event — this helps you see what network requests an element tap or screen transition triggered.

Query parameters:

ParameterTypeDefaultMaxDescription
tsinteger (required)Reference timestamp in Unix milliseconds. Flows near this time are returned
windowinteger3,00030,000How many milliseconds before and after ts to search (so window=5000 looks ±5 seconds)
session_idstringOptional Ghost session ID to scope the query. If omitted, searches the active session

Flow limit: Up to 500 flows are queried from the database for the time window.

Response:

[
{
"flow_id": "01HWXYZ...",
"method": "POST",
"host": "api.example.com",
"path": "/api/v1/checkout",
"status_code": 200,
"duration_ms": 245.5,
"time_delta_ms": -150.0,
"relevance": "primary",
"size": 4567,
"started_at": "2024-01-15T10:30:14.850000000Z",
"request_content_type": "application/json",
"request_body": "{\"cart_id\": \"abc123\"}",
"response_content_type": "application/json",
"response_body": "{\"order_id\": \"xyz789\", \"status\": \"confirmed\"}"
}
]
FieldTypeDescription
flow_idstringFlow identifier — click to jump to the flow in Ghost’s traffic list
time_delta_msnumberHow many milliseconds before (negative) or after (positive) the reference timestamp this flow started. Helps you see the sequence of events
relevancestring"primary" (likely related to the user action) or "noise" (probably background SDK traffic like analytics or push notifications)
sdk_categorystringIf the flow is noise, which SDK category it belongs to (e.g., “Firebase”, “Sentry”, “Analytics”)
request_body / response_bodystringFirst 2,048 bytes of the body as a preview. Binary content shows a placeholder like [binary image/png, 12345 bytes]

Results are sorted by absolute time proximity — flows closest to the reference timestamp appear first.

GET /api/v1/inspector/devices/{id}/interactions?since=300&limit=50

Returns recent touch and input events captured from the device — taps, scrolls, text input, screen transitions.

Query parameters:

ParameterTypeDefaultMaxDescription
sinceinteger3003,600How many seconds of history to return (300 = last 5 minutes)
limitinteger50200Maximum number of events to return

Response:

[
{
"timestamp": "2024-01-15T10:30:14Z",
"action": "tap",
"x": 540,
"y": 1200,
"element_class": "android.widget.Button",
"element_text": "Submit",
"element_id": "com.app:id/submit_button",
"detail": "",
"confidence": "high"
}
]
FieldTypeDescription
actionstringType of interaction: "tap", "long_press", "scroll", "text_change", "text_input", "screen_change", "value_change", "state_change", "key"
confidencestring"high" (from Android getevent kernel stream — direct hardware input) or "medium" (from iOS hierarchy diffing — inferred from UI changes)

The internal interaction log holds a maximum of 100 events — older events are dropped as new ones arrive.

GET /api/v1/inspector/devices/{id}/interaction-context?since=300&limit=20&session_id=01HWXYZ...

Returns a richer interaction sequence that includes selectors for each element and correlated network flows — designed specifically for generating test automation code.

Query parameters:

ParameterTypeDefaultMaxDescription
sinceinteger3003,600Seconds of history
limitinteger20100Maximum interaction entries
session_idstring(active)Ghost session for flow correlation. Falls back to the currently active session

Flow limit: Up to 500 flows are queried for correlation.

Response:

{
"device": {
"platform": "android",
"os_version": "14",
"app": "com.example.app",
"resolution": "1080x2400"
},
"interactions": [
{
"timestamp": 1705312814000,
"action": "tap",
"element_class": "android.widget.Button",
"element_text": "Submit",
"element_id": "com.app:id/submit_button",
"x": 540,
"y": 1200,
"selectors": [
{
"strategy": "accessibility_id",
"value": "Submit",
"reliability": "excellent",
"formats": {"appium_java": "..."}
}
],
"triggered_flows": [
{
"method": "POST",
"path": "/api/v1/checkout",
"status_code": 200,
"duration_ms": 245.5,
"flow_id": "01HWXYZ..."
}
]
}
]
}

Noise flows (SDK background traffic) are excluded from triggered_flows — only relevant network requests are shown.

POST /api/v1/inspector/devices/{id}/tap

Sends a tap event to the device at specific screen coordinates — like physically touching the screen at that point.

Request body (1,024-byte limit):

{
"x": 540,
"y": 1200
}

Both x and y are required (validated with pointer-based nil checks — sending 0 is valid, but omitting the field is an error).

Response: {"ok": true}

POST /api/v1/inspector/devices/{id}/input

Types text or sends a key press to the device. You must provide exactly one of text or key — not both, not neither.

Request body (4,096-byte limit):

{
"text": "hello@example.com"
}

Or:

{
"key": "enter"
}

Valid key values: "enter", "backspace", "tab"

Response: {"ok": true}

GET /api/v1/inspector/devices/{id}/webviews

Detects WebView elements in the UI hierarchy and (on Android) discovers debuggable WebView sockets through /proc/net/unix.

Response:

{
"webviews": [
{
"element_id": "node_42",
"class_name": "android.webkit.WebView",
"package": "com.example.app",
"inspectable": true
}
],
"sockets": [
{
"pid": 12345,
"socket_name": "webview_devtools_remote_12345"
}
]
}

Detected WebView classes:

  • Android: android.webkit.WebView, android.webkit.WebViewClient, com.tencent.smtt.sdk.WebView (Tencent X5), com.uc.webview.export.WebView (UC Browser engine)
  • iOS: XCUIElementTypeWebView, plus any class containing “WebView” or “SafariViewController”

The sockets field is Android-only and shows debuggable WebView DevTools sockets.

POST /api/v1/inspector/devices/{id}/bug-report

Generates a comprehensive bug report by combining device context, screenshots, element information, correlated network traffic, interaction history, and optional AI enhancement.

Request body (64 KB limit):

{
"node_id": "node_42",
"flow_ts": 1705312815000,
"window_ms": 300000,
"offset": 0,
"session_id": "01HWXYZ..."
}
FieldRequiredDefaultDescription
node_idNoElement ID for which to generate selectors. If omitted, the report includes traffic and interactions but not element-specific data
flow_tsNonowReference timestamp (Unix ms) for flow correlation. Defaults to the current time
window_msNo300,000Correlation window in milliseconds. Default is 5 minutes (300,000 ms)
offsetNo0Screenshot ring buffer offset (0 = latest)
session_idNoGhost session to scope flow queries

What the handler does:

  1. Gathers device info — platform, OS version, app, screen resolution
  2. Captures screenshot — from ring buffer at the specified offset (failure is non-fatal)
  3. Fetches element + selectors — if node_id is provided
  4. Queries correlated flows — up to 200 flows within the correlation window
  5. Collects interactions — from the last 5 minutes
  6. Generates the bug report — deterministic assembly of all gathered data
  7. Starts GIF encoding — runs in a background goroutine alongside AI enhancement
  8. AI enhancement — 60-second timeout. If the LLM is available, it enhances the report title, description, and summary. Falls back gracefully if AI times out or fails

GIF details:

  • Max size: 5 MB
  • Max frames: 30 (from the ring buffer)
  • Scale factor: 0.25× (quarter resolution to save size)
  • Frame delay: 500ms between frames (50 centiseconds)

The report is automatically saved as an artifact (type "bug_report", format "json"), and an artifact.created WebSocket event is broadcast.

Response: A comprehensive bug report object including:

FieldDescription
titleBug report title (AI-enhanced if available)
descriptionDetailed description
device_contextDevice, OS, app, resolution, timestamp
summaryExecutive summary
screenshot_b64Base64-encoded screenshot image
elementThe targeted element (if node_id was provided)
selectorsTest automation selectors for the element
api_evidenceNon-error primary correlated flows
findingsDetected issues (error/warning/info with messages)
errorsFailed HTTP requests promoted as errors (status 4xx/5xx, first ~200 chars of body)
repro_stepsGenerated reproduction steps with types (action/observation/network), timestamps, triggered flows, and inferred flags
waterfall_dataTiming waterfall for correlated flows
markdownFull report as markdown text
screenshot_gif_b64Base64-encoded animated GIF of recent screenshots
ai_enhancedWhether AI enhancement succeeded
artifact_idID of the saved artifact
created_atWhen the report was generated

These endpoints help set up mobile devices for traffic capture — discovering simulators/emulators, installing certificates, and configuring proxy settings.

GET /api/v1/mobile/platform

Returns the host operating system and CPU architecture. Used by the frontend to show platform-appropriate setup instructions.

Response:

{
"os": "darwin",
"arch": "arm64"
}
GET /api/v1/mobile/verify

Verify that a device is routing traffic through Ghost’s proxy. The device hits this endpoint through the proxy — if it succeeds, the proxy connection is working.

Response:

{
"connected": true,
"client_ip": "192.168.1.50"
}
GET /api/v1/mobile/profile

No authentication required — this endpoint is accessed from Safari on the iOS device, which doesn’t have Ghost’s auth token.

Generates and serves an Apple Configuration Profile (.mobileconfig) containing Ghost’s CA certificate. Opening this in Safari triggers the iOS certificate installation flow.

The profile contains the certificate only — no proxy configuration (global HTTP proxy payloads require a supervised device, so proxy settings must be configured manually in Wi-Fi settings).

Content-Type: application/x-apple-aspen-config Content-Disposition: attachment; filename="ghost-proxy.mobileconfig"

Each download generates fresh random UUIDs for the profile identifiers.

GET /api/v1/mobile/simulators

Lists all currently booted iOS simulators. Uses xcrun simctl list devices --json with a 10-second timeout.

Response:

{
"simulators": [
{
"udid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"name": "iPhone 16 Pro",
"runtime": "iOS 17.2",
"state": "Booted"
}
],
"available": true
}

Returns {"simulators": [], "available": false} on non-macOS systems or when Xcode command-line tools aren’t installed.

POST /api/v1/mobile/simulators/install-cert

Installs Ghost’s CA certificate into an iOS simulator’s keychain using xcrun simctl keychain <udid> add-root-cert.

Request body (1,024-byte limit):

{
"udid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
}

The UDID is validated against ^[A-Za-z0-9][A-Za-z0-9.:_-]*$ to prevent command injection (rejects values starting with - which could be interpreted as flags).

Response: {"ok": true}

GET /api/v1/mobile/emulators

Lists connected Android emulators. Uses adb devices with a 10-second timeout and filters for serials starting with emulator-.

Response:

{
"emulators": [
{
"serial": "emulator-5554",
"state": "device"
}
],
"available": true
}

Returns {"emulators": [], "available": false} when ADB is not installed.

POST /api/v1/mobile/emulators/set-proxy

Configures an Android emulator to route HTTP traffic through Ghost. Uses adb shell settings put global http_proxy 10.0.2.2:{proxyPort} — the address 10.0.2.2 is Android emulator’s special alias for the host machine’s loopback (localhost).

Request body (1,024-byte limit):

{
"serial": "emulator-5554"
}

Response: {"ok": true}

POST /api/v1/mobile/emulators/clear-proxy

Removes the HTTP proxy setting from an Android emulator using adb shell settings put global http_proxy :0.

Request body (1,024-byte limit): {"serial": "emulator-5554"}

Response: {"ok": true}

POST /api/v1/mobile/emulators/push-cert

Pushes Ghost’s CA certificate file to the emulator’s SD card using adb push. The certificate is pushed to /sdcard/ghost-ca.crt — the user must then manually install it from Android Settings > Security > Install from storage.

Request body (1,024-byte limit): {"serial": "emulator-5554"}

Response: {"ok": true}

EventTriggerPayload
device.discoveredNew device found during discovery scanDevice info object
device.updatedDevice state changed (connecting, error, etc.)Updated device info
device.connectedInspector successfully connected to deviceDevice info with state: "connected"
device.disconnectedInspector disconnected from deviceDevice info with state: "disconnected"
device.removedDevice disappeared from discoveryDevice info
artifact.createdBug report auto-saved as artifactArtifact summary (id, type, title, format, size, metadata)
LimitValueContext
Connect timeout120 secondsDevice connection (WDA build can be slow)
AI enhancement timeout60 secondsBug report AI analysis
Mobile command timeout10 secondsAll xcrun/adb commands
Screenshot buffer30 framesRing buffer size per device
Interaction log100 eventsPer-device in-memory cap
Hierarchy log100 snapshotsPer-device in-memory cap
Correlation flow limit500 flowsMax flows queried for correlation
Bug report flow limit200 flowsMax flows in bug reports
Interaction context flow limit500 flowsMax flows for test code generation
Correlation default window3 seconds±3s around reference timestamp
Correlation max window30 secondsMaximum correlation window
Bug report window5 minutesDefault correlation window for bug reports
GIF max size5 MBAnimated screenshot GIF cap
GIF max frames30Maximum frames in GIF
GIF scale factor0.25×Quarter-resolution to save size
Body preview2,048 bytesFirst 2 KB of request/response in correlations
Tap body limit1,024 bytesRequest body for tap command
Input body limit4,096 bytesRequest body for text/key input
Bug report body limit64 KBRequest body for bug report generation
Mobile action body limit1,024 bytesAll mobile setup POST endpoints
Interactions default50 events, 5 minDefault limit and time window
Interactions max200 events, 1 hourMaximum limit and time window
Interaction context default20 entries, 5 minDefault for test code generation
Interaction context max100 entries, 1 hourMaximum for test code generation