Device HTTP API
These routes belong to the device HTTP server, not the public MCP URL.
The device server uses port 8080. Use the device's reachable local-network address or a direct USB port forward from your computer. Send Content-Type: application/json for JSON POST bodies and URL-encode query parameters. Each route below lists its HTTP method.
Connection and capability discovery
Browse the HTTP endpoint reference for an individual page per method/path, with parameters, responses and curl examples.
Use a user-provided base URL, such as http://DEVICE_LAN_IP:8080. For direct USB access, forward the device's HTTP port to a computer port; for example, iproxy 18080:8080, then use http://127.0.0.1:18080. This does not require SSH. Keep the forward running. Device pairing/trust and a working USB connection are required. A computer's localhost is not the device unless a forward exists.
GET /automation returns apiVersion: 1, supported features, and size limits. Use GET /endpoints to discover the installed endpoint set. The following endpoints support the HTTP-only MCP and any client that can send HTTP requests.
Apps, buttons, gestures, and full screenshots
| Method | Path | Input / result |
|---|---|---|
| GET | /apps/list?query=...&limit=100 | Installed {apps:[{bundleId,name}],limit}; limit 1..300 |
| POST | /apps/open | {bundleId,restart:false}; returns {opened,front,restarted} |
| POST | /device/button | {button:"home"}; also power, volume_up, volume_down |
| POST | /device/touch | {x,y} for a tap; add endX,endY,duration for a swipe |
| GET | /device/screenshot | Full-device PNG, including overlays; available independently of an app's element tree |
A restart explicitly terminates an app and may discard unsaved state. Opening an app does not prove its contents have loaded; verify the returned foreground identity and wait for a meaningful element. Gesture coordinates use native interface pixels from a fresh screenshot. duration is 0.05..3 seconds, default 0.35, and both swipe end coordinates are required. Buttons and gestures do not bypass a passcode. Inspect the resulting screen after every action.
Files and project directories
All file paths are relative to the user's Scripts folder. Absolute paths, parent traversal, symbolic-link traversal, and managed execution files are rejected. Do not use a device-specific absolute filesystem prefix. Parent folders are created when writing a file, so writing Demo.aso/lib/helper.js can create an organized project without a separate upload step.
| Method | Path | Input / result |
|---|---|---|
| GET | /files/list?path=&limit=100&offset=0 | {directory,entries,total,nextOffset}; entries include path, name, directory, size, modifiedAt |
| GET | /files/read?path=Demo.aso/main.js | {path,content,encoding,bytes,sha256,offset,truncated,nextOffset} |
| GET | /files/download?path=... | Raw file bytes |
| POST | /files/write | {path,content,encoding:"utf8",overwrite:false,expectedSha256?}; returns {path,bytes,sha256} |
| POST | /files/mkdir | {path}; create a directory or project folder |
| POST | /files/move | {path,destination,expectedSha256?}; destination must not exist |
| POST | /files/delete | {path,expectedSha256?}; delete one file or an empty directory |
Files are limited to 8 MiB. Read/write encodings are utf8 and base64; binary assets should use Base64 or the download endpoint. Reads accept byte offset and maxBytes (1..524288; default 262144). Follow nextOffset when truncated is true. For UTF-8 text, use the returned offset rather than adding a character count. Listing accepts limit 1..500 and offset 0..10000.
Creating a file never replaces an existing one. To replace it, first read its current sha256, then send overwrite:true and that value as expectedSha256. A changed or missing file returns HTTP 409. Deleting or moving a regular file also requires its current hash. Empty directory deletion needs no hash and is never recursive. Active script/project paths cannot be changed. Stop related runs first, and delete retained automated runs before moving their project folder. Preserve unrelated files and unsaved editor work.
{
"path": "Demo.aso/main.js",
"content": "console.log('Ready');\n",
"encoding": "utf8",
"overwrite": false
}JavaScript execution with results
| Method | Path | Input / result |
|---|---|---|
| POST | /automation/run | {id?,source?,path?,args?}; returns {runId,nativeRunId,state,sourcePath} |
| GET | /automation/runs?id=... | One run's state, running, result, logs and metadata |
| GET | /automation/runs?limit=20 | Retained run IDs, newest first; limit 1..64 |
| POST | /automation/stop | {runId}; request cancellation of that run only |
| POST | /automation/delete | {runId}; remove a finished run and its execution artifacts, preserving the original script |
Provide either an async JavaScript function body in source, or a saved .js path. Bodies may use return and await. Saved scripts keep their original relative-import base and are not overwritten. Use /scripts/run for the normal .js or .aso/.cso launcher, including repeats; use /automation/run for a structured returned value, per-run console output and an error stack.
Source is limited to 256 KiB. Results must be JSON-serializable and fit within 128 KiB of serialized text. Convert native proxies with .toJS() and exact large integers to strings. Console output retains up to 200 events and 64 KiB of text. The result contains ok, value or error, runId, timestamps, logs, and droppedLogEntries. Errors include name, code, message, stack, line, and source URL when available. Logs are also available while a run is active.
{
"source": "console.log('Test started'); return {foreground: frontMostAppId()};",
"args": {"label": "example"}
}Arguments use the same immutable string args / at.args contract as the normal launcher. A submission acknowledges acceptance, not completion. Poll its own runId until running is false. States are running, finishing, succeeded, failed, cancelled, or interrupted; an initial acknowledgement uses submitted. Cancellation is cooperative and does not undo completed work. A run that finishes before cancellation may still report success.
Choose a UUID id before submission when recovering from a lost connection matters. Both standard UUID notation and 32 hexadecimal characters are accepted; the returned runId is canonical lowercase hexadecimal. A retained ID cannot be submitted twice. After an uncertain submission, inspect that ID and the actual screen before deciding what to do. Do not replay a mutation blindly.
Up to 64 execution records are retained until explicitly deleted. Records remain available after reconnecting the MCP or restarting the device automation service. An active run interrupted by a device/service restart may have no completion record and reports interrupted; that is not success. Collect error evidence before deleting a record. An active run cannot be deleted.
Crash reports
| Method | Path | Result |
|---|---|---|
| GET | /diagnostics/crashes?limit=20 | {reports:[{reportId,name,modifiedAt,bytes}]}; limit 1..100 |
| GET | /diagnostics/crash?id=...&offset=0&maxBytes=65536 | {reportId,content,offset,truncated,nextOffset} |
Use the opaque reportId returned by the list; arbitrary system paths and report deletion are not supported. Only reports accessible to ApplesauceOS are listed. Report pages accept 1024..524288 bytes and offsets up to 64 MiB. Follow nextOffset for large reports. Check the process name and timestamp before attributing an existing report to a test. A JavaScript exception is not itself an iOS process crash.
HTTP failures
Check the HTTP status and JSON error response. Invalid inputs return 400, unavailable/forbidden files 403 or 404, conflicts and active-file changes 409, and oversized files 413. UI errors may include a specific error code and message; file/execution errors include an error message. Re-observe after a stale reference and re-read after a hash conflict. Fix invalid inputs rather than repeatedly sending them.
Status & Info
| Endpoint | Description | Example Response |
|---|---|---|
GET / | Browser landing page with endpoint links | HTML |
GET /endpoints | All registered method/path pairs, read directly from the router | {port:8080, routes:[{method,path}], options} |
GET /isBusy | Script status; busy/retV exclude daemon scripts | {"retV":false,"busy":false,"count":0,"scripts":[]} |
GET /runningScripts | List running script paths | ["MyAutomation.aso"] |
GET /lastError | Last JS error from engine | {"lastError":"..."} |
GET /deviceInfo | Same object as getDeviceInfo() | serialNumber, systemVersion, screen, battery, etc. |
GET /SN | Serial number (plain text) | DEVICE_SERIAL |
GET /IP | Local WiFi IP (plain text) | 192.168.1.42 |
GET /tweakVersion | Installed ApplesauceOS version | Plain text |
GET /appInfo?bundleId=... | Same object as appInfo(bundleId) | App metadata or installed:false |
GET /temperature | Device temperature; negative value if unavailable | {"celsius":32.5} |
Each /isBusy entry contains path, fullPath, entryPath, daemon, stopRequested, and runningSeconds. count includes daemon scripts. /runningScripts returns the original path strings used to start them.
Script Control
| Endpoint | JSON body / query | Description |
|---|---|---|
POST /scripts/run | {path,args?,id?,repeat?,interval?} | Start a relative .js file or .aso/.cso directory under Scripts; returns {id,path,fullPath,entryPath} |
GET /scripts/runs | Optional id query | Without id: {runs:[...]}; with id: one execution journal, including output and errors |
POST /scripts/stop | {id} | Request cancellation of that execution; returns {stopRequested} |
GET /stopAll | — | Request cancellation of all non-daemon scripts |
curl 'http://127.0.0.1:8080/scripts/run' \
-H 'Content-Type: application/json' \
-d '{"path":"MyAutomation.aso","args":{"name":"Alex","count":5,"enabled":false},"repeat":1}'
curl 'http://127.0.0.1:8080/scripts/runs?id=RUN_UUID'
curl 'http://127.0.0.1:8080/scripts/stop' \
-H 'Content-Type: application/json' \
-d '{"id":"RUN_UUID"}'path must be relative, remain inside Scripts, and end in .js, .aso, or .cso. An .aso or .cso path is a directory: its entry is main.js, then index.js. A missing entry fails instead of executing an arbitrary helper. Starting a project and its entry concurrently is rejected as a duplicate. HTTP execution reads saved files; App Run saves project drafts before submitting.
repeat: integer 0..10000, default 1; 0 repeats until stopped.interval: 0..3600 seconds between iterations. When omitted, a directory's
info.plist RepeatInterval may supply its default; otherwise 0.
id: optional UUID chosen by a client before submission. IDs are canonicalized
to uppercase UUID notation. A retained ID cannot be submitted again: inspect its journal instead of replaying an action after a lost response.
args: optional object. Its keys are argument names; its values become strings
in the script's reserved, immutable args namespace. at.args is the same object.
Arguments in memory
Every run receives its own read-only argument object, available to main and all required modules. Nothing automatically creates or reads an argument file. Argument names do not become global variables, so they cannot replace automation functions or launcher options. Use bracket notation for names with spaces.
| Submitted value | Value exposed to JavaScript |
|---|---|
"hello", "", "007" | Unchanged strings |
5, false, null | "5", "false", "null" |
| JSON object / array | Compact JSON string; parse explicitly when needed |
| Missing key | undefined |
console.log(args.name); // "Alex"
const count = Number(args.count); // 5
const enabled = args.enabled === "true"; // false
console.log(at.args === args); // trueDo not use Boolean(args.enabled) to parse a boolean: "false" is a nonempty string. JSON.parse(args.config) can reconstruct a structured argument. Assigning, replacing, deleting, or extending the argument namespace is unsupported; strict JavaScript throws for attempted mutation. A module can read args directly or accept explicit function parameters. Concurrent runs have independent values.
Submission acknowledges startup, not successful completion. Invalid arguments return HTTP 400. Missing entries, duplicate submissions, or launch failure return
- Query the returned ID to determine the outcome. Stop is cooperative; a pure
busy loop without cancellation checks can delay stopping.
Journals retain 30 finished runs plus active runs in memory and reset when SpringBoard restarts. Fields include id, path, entryPath, state (running, completed, failed, stopped), startedAt, finishedAt after completion, logs, and droppedLogs. Failures include scriptError, errorSource, and errorLine. List summaries omit logs. At most eight journaled runs may be active. The shared log is available through /logs; use a run ID for attributed results.
Logs
| Endpoint | Params | Description |
|---|---|---|
GET /logs | lines=N (default 200, 0=all) | Tail of script log (plain text) |
GET /logs/clear | — | Clear the log |
Logs are shared by scripts. Use a unique run ID in your own completion/error messages, poll that run's path, and verify the intended UI outcome. /lastError is historical engine state, not a per-run success result. Clearing logs does not clear that field.
Settings
Synthetic touch indicators are always active and cannot be disabled through settings or JavaScript.
The installed ApplesauceOS app provides these controls without requiring a computer. Its Blacklist disables the helper for selected bundle identifiers. Fully close and reopen an app after changing its blacklist entry. UIHelper and RemoteObjC/RemoteC calls are unavailable for blocked apps. Coordinate touch and visual automation remain available. Obtain fresh elements and reconnect native clients after enabling the helper again.
Avoid safe mode and Show IP in status bar are disabled by default. Recovery changes apply after restarting SpringBoard. Other switches update running processes through notifications and cached preferences.
Keep screen awake prevents automatic display sleep while enabled; it does not unlock a device or prevent manually locking it. It is disabled by default.
GET /settings returns the preference snapshot; POST /settings applies a validated partial update. Boolean keys are avoidSafeMode, keepAwake, debugLog, helperEnabled, serverEnabled, inspectorShortcut, volumeStop, showIPInStatusBar, errorAlerts, and scriptToasts. disabledHelperBundleIDs is an array of bundle identifiers. Writes preserve unrelated settings and are serialized across processes. Turning serverEnabled off disconnects new HTTP/MCP requests; it can be enabled again from the app.
GET /settings/logs reads at most the last 128 KiB of the script log. POST /settings/action accepts {action:"inspector"}, {action:"stopScripts"}, or {action:"respring",confirm:true}. Restarting SpringBoard ends active scripts.
| Endpoint | Params | Description |
|---|---|---|
GET /setDebugLog | on=0|1 | Toggle verbose [ApplesauceOS] system log |
GET /setErrorAlert | on=0|1 | Toggle error alert popups |
GET /setScriptToasts | on=0|1 | Toggle script start/stop toasts |
GET /setKeepAwake | on=0|1 | Persist ApplesauceOS's keep-awake preference |
GET /setSafeMode | on=0|1 | Set avoidSafeMode for the next SpringBoard launch; on=1 enables avoidance, not safe mode |
Omitting on reads the current value for every setting above except /setErrorAlert, which requires it. Temporary scripts should restore the preferences they change. These settings are not performance or crash guarantees.
---
UIHelper inspection and actions
Press Volume Up + Volume Down together to open or close the on-device UIHelper inspector. A chord toggles once until both buttons are released. Holding either button alone for 0.2 seconds still stops scripts. The HID monitor observes the buttons; normal system volume behavior is not suppressed.
The inspector captures the foreground app and displays a frozen screenshot at the available screen width. Drag to pan; pinch or double-tap to zoom. UIHelper/OCR allow up to 4x; Coordinates allows substantially higher zoom, reaching up to 32 interface points per source pixel (capped at 128x). Fit shows the entire image. These gestures manipulate the captured image, not the underlying app. Zoom uses gestures; there are no +/− buttons. Tap the image to select an element without shrinking the screenshot. Picking prioritizes named controls and accessibility elements over decorative views, including SwiftUI drawing layers. Within each priority, smaller and deeper elements come first. Previous / Next cycles through overlapping elements and containers. Details opens a separate panel with larger text; Back to screen captures the app again and clears the old selection. Zoom and position are preserved when the image and viewport dimensions are unchanged; select an element from the refreshed image. Refresh also captures again and resets the viewport. The details show UIHelper attributes, supported operations, candidate selectors and their match counts at capture time. Test queries the selected suggestion against the live app; Read retrieves current text; Copy JS copies a UIHelper snippet using a unique selector. When none is unique, it copies an explanatory comment instead of selecting an arbitrary match. Tap hides the inspector, acts through UIHelper and refreshes the snapshot. Refresh captures again; Close dismisses it. Test only counts matches; it does not tap. The selected name is followed by its center x/y in native touch pixels. Copy XY copies x, y; zoom and panning do not alter those coordinates. In OCR mode, the primary button is Copy Rect and the displayed geometry is x, y, width, height. Copy Rect produces a JSON region in native pixels, with edges rounded outward and clipped to the screen. The highlighted box uses those same bounds. Pass the copied object as region with ocr({coordinates:"pixels", region: ...}). A separate Copy XY button between Previous and Next copies the text rectangle's center for touches. The mode switch offers:
- UIHelper: the accessibility tree, selectors, and existing UIHelper actions.
- Coordinates: pick any native pixel, copy
tap(x, y), or send a simulated
touch. Enlarged pixels use nearest-neighbor sampling, a grid appears at high zoom, and the selected pixel is outlined at its actual size. The four arrows replace Previous/Next and move exactly one native pixel per press. Double-tap jumps to pixel-level zoom. Refreshing captures only the screen, without a helper query. Coordinates identify image pixels: taps inside [x,x+1) and [y,y+1) select (x,y), independently of the displayed magnification.
- OCR: recognize visible text in the current image once in the background,
then select a text box. Copy its text or code that calls ocr({level:0, coordinates:"pixels"}), requires one exact text match, and taps its center. Results are reused until the next capture; errors are shown without disabling coordinate picking.
In Coordinates Details, Copy XY replaces Test. In OCR Details, Copy Rect replaces Test and Copy text replaces Read. The separate Copy XY center button remains available. Tap sends a simulated touch rather than a UIHelper operation. Coordinate/OCR modes also work with a blacklisted app or no accessible elements. Insert code into script offers coordinate taps for every selection, OCR lookup/tap for recognized text, and UIHelper actions when a unique selector is available. Inserted snippets have their own block scope. Fixed coordinates depend on device size, orientation, and layout. OCR code searches the live screen when executed; it does not reuse a saved text position. Recognition locates text, not the tappable area of its associated control. Test the center or choose the actual target in Coordinates mode (for example, an icon instead of its label).
External UIHelper mutations are rejected while the inspector is visible, to avoid sending injected taps into the overlay. Scripts may still read state. The open inspector retains its current observation independently of the HTTP cache, releasing it on refresh or close. It does not expire after 60 seconds or disappear when HTTP clients request other observations. Element and app identity checks still apply; refresh when the underlying UI changes or its native reference is no longer available. The inspector window is excluded from UIHelper's own hierarchy.
For script-side observation:
const ui = UIHelper.forApp("com.apple.Preferences");
const observation = ui.observe({limit: 300, includeContainers: false});
console.log(JSON.stringify(observation));For an AI or another runtime client, use the existing ApplesauceOS HTTP server:
Omit bundleId to use the foreground app, or provide it to require that app to be foreground. /ui/query and /ui/wait require an object selector; use {name:"..."} instead of the JavaScript string shorthand. Observation flags default to false; screenshots are requested explicitly.
| Route | Behavior |
|---|---|
GET /ui | Discover the UIHelper endpoint contract. |
GET /ui/observe or POST /ui/observe | Capture structured foreground-app elements. Options: bundleId, limit (1..1000, default 300), includeContainers, includeHidden, screenshot. GET boolean flags accept 1 or true; POST uses JSON booleans. |
GET /ui/screenshot?observationId=... | PNG for an observation requested with screenshot: true. |
POST /ui/query | {bundleId, selector}; return live match count, uniqueness and up to 50 match summaries. Uses exactly UIHelper selector semantics. |
GET /ui/element?observationId=...&ref=e1 | Read fresh attributes for an observed element. |
POST /ui/action | {observationId, ref, action, text?, direction?}; actions: read, tap, setText, typeText, clear, scroll, scrollIntoView. Mutations are never retried automatically. |
POST /ui/wait | {bundleId, selector, condition, timeoutMs, intervalMs}; condition is exists or absent. Defaults: 5000/200 ms; limits: timeout 0..10000, interval 100..2000 ms. |
POST /ui/inspector | {action: "open"} or {action: "close"} schedules the overlay operation. |
curl 'http://localhost:8080/ui/observe?includeContainers=0&screenshot=1'
curl -X POST 'http://localhost:8080/ui/query' \
-H 'Content-Type: application/json' \
-d '{"bundleId":"com.example.app","selector":{"id":"login"}}'
curl -X POST 'http://localhost:8080/ui/action' \
-H 'Content-Type: application/json' \
-d '{"observationId":"COPY_FROM_OBSERVE","ref":"e3","action":"tap"}'Observations contain schemaVersion, observationId, bundleId, pid, processSessionId, timestamps, screen geometry and flat elements with short ref values, parentRef/depth, attributes, rectangles, action names and selector suggestions. HTTP and script-side observations share the same server cache and omit internal element IDs. A script can pass its observation ID and references to an HTTP client. Short references are meaningful only together with their observation ID. Copy a suggested selector into findElement or waitForElement for a reusable script.
| Observation field | Meaning |
|---|---|
backend, schemaVersion | "UIHelper", currently 1 |
observationId, bundleId, pid, processSessionId | Snapshot and process identity |
timestamp, screenshotTimestamp | Unix seconds; screenshot timestamp is present only when requested |
captureDurationMs, expiresInSeconds | Capture duration and nominal cache lifetime |
screen | {width, height, scale, coordinateSpace:"nativePixels"} |
scanned, eligible, truncated, completeTraversal | Traversal/output coverage |
screenshotURL | Relative PNG endpoint, present only when requested |
elements | Flat element records; parentRef is omitted when no included ancestor exists |
Each element includes ref, depth, identifier, type, className, label, text, value, rect, visible, enabled, accessible, selected, focused, index, actions, and selectors. Selector suggestions have {selector, matches, unique, verified, stability}; stability is identifier, label, or class. Use suggestion.selector, not the entire suggestion object, as the query selector.
/ui/element and /ui/action return {backend, observationId, ref, action, value}. Reads put fresh attributes in value; successful mutations put true there. This acknowledges the action, not completion of subsequent navigation. /ui/wait success returns {matched:true, condition, count, elapsedMs}. A screenshot request returns PNG bytes, or 404 if it expired or was not included in the observation.
The server retains observations for up to 60 seconds, at most eight entries; screenshots also have a 12-million-pixel aggregate cache budget (one screenshot may exceed that budget). Old entries may be evicted earlier. References retain UIHelper's process and detached-view checks. Before a reference operation, the helper checks the element's identifier, class, type, label and rectangle in its main-thread action handler. A changed/recycled element, changed foreground app or expired observation returns a stale-reference error; observe again.
Snapshots scan at most 5000 nodes and return at most the requested limit, within the response size limit. truncated reports omitted output; completeTraversal indicates whether selector counts covered the traversed tree completely. Suggestions are marked unique only after a complete traversal. Match counts are capture-time evidence, not a guarantee across navigation or app updates. Visibility follows UIHelper's frame/clipping checks, not full occlusion testing. Secure text field text/value are omitted from inspection responses.
Screenshots are opt-in. Screenshot and tree captures occur close together but are not atomic; timestamps and capture duration are included. Foreground-app or screen-geometry changes during capture reject the observation. Close the inspector before requesting an HTTP screenshot. Rectangles use native pixels; the inspector maps those coordinates through the screenshot's zoom and pan.
The endpoints do not launch apps, continuously capture screenshots, or poll the full UI tree in the background. /ui/wait polls only its selector; an individual helper request can outlast the polling deadline. Errors return JSON with error and message, with HTTP 400 for invalid requests, 409 for stale/non-interactable observations, 408 for timeouts, and 503 for an unavailable helper. A failed wait includes its last query for diagnosis.