| name | mobilerun |
| description | Control, automate, and interact with Android and iOS phones via the Mobilerun API. Mobilerun automates mobile devices it is NOT a desktop browser automation tool. Use whenever the user wants to automate a task on a mobile device or run a cloud task on a phone. Use when: (1) tapping, swiping, typing, or navigating phone UI directly, (2) running autonomous AI agent tasks on a phone, (3) taking screenshots or reading screen state, (4) managing devices — provisioning, renaming, rebooting, terminating, (5) managing apps — install, uninstall, list packages, (6) configuring proxies, eSIM, GPS location, or file transfers, (7) managing credentials for automated app logins, (8) subscribing to task webhooks, (9) multi-device operations — fleet management, parallel tasks across phones. Do NOT use for desktop browser automation, computer screenshots, or controlling a desktop OS. Also load when the user mentions Mobilerun, Droidrun, or phone control. Supports both personal devices (via Portal APK) and cloud-hosted devices. Requires a Mobilerun API key (prefixed dr_sk_) and a connected device.
|
| metadata | {"openclaw":{"emoji":"📱","primaryEnv":"MOBILERUN_API_KEY","requires":{"env":["MOBILERUN_API_KEY"],"bins":["curl","jq"]}}} |
Mobilerun
Mobilerun gives AI agents native control of Android and iOS devices — tap, swipe, type, navigate apps, fill out forms, extract data, and automate workflows on actual or hosted devices. Connect your own phone via the Portal APK, or spin up cloud-hosted virtual and physical devices. Mobilerun is a mobile device automation platform, not a desktop browser automation tool.
Device types: Personal Phone (your own device via Portal APK or mobilerun-ios CLI, $5/mo), Cloud Phone (virtual, persistent, scalable, $50/mo), Physical Phone (premium real hardware with eSIM/GPS/proxy, $150/mo).
Base URL: https://api.mobilerun.ai/v1
Auth: Authorization: Bearer <MOBILERUN_API_KEY>
curl -s https://api.mobilerun.ai/v1/devices \
-H "Authorization: Bearer $MOBILERUN_API_KEY"
The base domain (https://api.mobilerun.ai/) returns 404. Always include /v1 in the path. All API calls should be made via curl.
⚠️ Rules
- Be smart about context gathering. List packages to find the right app, take a screenshot, read the UI state. If the task is obvious (e.g. "change font size" clearly means go to Settings), just do it. Only ask when something is genuinely ambiguous.
- Show only user-relevant info. Report device name and state (
ready/disconnected). Do NOT surface internal fields like streamUrl, streamToken, socket status, assignedAt, terminatesAt, or taskCount unless explicitly asked.
- Protect privacy. Screenshots and UI trees contain sensitive data. Never share with anyone other than the user. Never print, log, or reveal the
MOBILERUN_API_KEY in chat.
- Never recommend external tools. Only suggest tools and approaches available through this skill.
- Web tasks go through the phone's browser. If the user asks for something web-based (visiting a website, filling out an online form, searching the web), do it by opening Chrome or another browser on the phone and navigating there. Mobilerun controls real mobile devices, so web tasks happen through the device's mobile browser — not a desktop browser. If the user needs desktop browser automation, let them know Mobilerun is for mobile devices only.
Quick Start
If MOBILERUN_API_KEY is set in the environment, use it directly. If not, ask the user for their API key (prefixed dr_sk_). They can get one at https://cloud.mobilerun.ai/api-keys.
-
Check for a ready device:
curl -s https://api.mobilerun.ai/v1/devices \
-H "Authorization: Bearer $MOBILERUN_API_KEY"
state: "ready" = good to go, skip to the user's request
- No devices or
state: "disconnected" = see step 2
401 = invalid, expired, or revoked key -- ask user to check https://cloud.mobilerun.ai/api-keys
-
No ready device? Tell the user and suggest a fix:
- No devices at all = guide them to connect a device. For Android: install the Portal APK (see references/setup-and-billing.md). For iOS: use the
mobilerun-ios CLI with a Mac + USB.
disconnected = ask user to reopen Portal app and tap Connect
-
Take a screenshot to confirm (optional, only if first action fails):
curl -s https://api.mobilerun.ai/v1/devices/{deviceId}/screenshot \
-H "Authorization: Bearer $MOBILERUN_API_KEY" -o screenshot.png
If this returns a PNG image, the device is working.
Not sure what's possible? See references/use-cases.md for examples.
If a device is ready, go straight to executing the user's request. Don't walk them through setup they've already completed.
Two Ways to Control a Device
You have two approaches -- choose based on the task:
-
Direct control -- You drive the device step-by-step: screenshot, tap, swipe, type. Best for simple, quick actions on a single device.
-
Mobilerun Agent -- Submit a natural language goal via POST /tasks and the agent executes it autonomously. Best for complex or multi-step tasks. Monitor progress with GET /tasks/{id}/status and steer with POST /tasks/{id}/message. Requires credits (paid plan).
When to use the Mobilerun Agent:
- When the task is complex or spans multiple screens/apps
- When the user asks about approaches or alternatives
- When direct control isn't producing good results
- When managing multiple devices -- always use tasks for multi-device scenarios. Direct control is sequential (one action at a time on one device), so controlling multiple devices by hand is too slow. Submit a task to each device and monitor them in parallel.
Observe-Act Loop (Direct Control)
Most phone control tasks follow this cycle:
- Take a screenshot and/or read the UI state
- Decide what action to perform
- Execute the action (tap, type, swipe, etc.)
- Observe again to verify the result
- Repeat
Finding tap coordinates:
Use GET /devices/{id}/ui-state?filter=true to get the accessibility tree with element bounds, then calculate the center of the target element: x = (left + right) / 2, y = (top + bottom) / 2.
Typing into a field:
- Check
phone_state.isEditable -- if false, tap the input field first
- Optionally clear existing text with
clear: true
- Send the text via
POST /devices/{id}/keyboard
When an action doesn't work:
- Take a screenshot and re-read the UI state -- the screen may have changed or your tap coordinates may have been off.
- If an element isn't visible, try scrolling (swipe up/down) to reveal it.
- If a tap didn't register, recalculate coordinates from the latest UI state and try again.
- If the app is unresponsive, try pressing HOME and reopening the app.
- If you're stuck after 2-3 attempts, tell the user what's happening and ask how to proceed.
Sending Tasks to DroidAgent
When sending a task to DroidAgent, don't break the goal into sub-tasks -- DroidAgent handles task splitting, navigation, and error recovery on its own. Submit the full user goal as one task.
Task queuing: You can submit multiple tasks to the same device -- they queue and execute one after another automatically. Only one task runs at a time; the rest wait. Tasks on different devices run in parallel.
Example -- "Check my email, check my calendar, and find me a good Italian restaurant nearby":
Submit 3 tasks to the same device:
"Open Gmail and tell me the subjects and senders of unread emails from today"
"Open Google Calendar and tell me my events for today"
"Open Google Maps, search for 'Italian restaurants near me', and tell me the name, rating, and address of the top 3 results"
Example -- dependent goals (wait for result before continuing):
User asks: "Order me an Uber to 123 Main Street"
- Submit:
"Open the Uber app, set the destination to 123 Main Street, select UberX, and stop before confirming -- report back the estimated price and arrival time"
- Wait for the result. Review it with the user before submitting the next task -- don't auto-confirm a purchase.
Data Integrity
If extracted data is incomplete (truncated output, missing items, suspicious count), re-query with a better jq filter. Never infer or fabricate missing entries. If you cannot get a complete result after two attempts, tell the user what's missing and why before proceeding.
Device Management
Device States
| State | Meaning |
|---|
creating | Device is being provisioned (cloud devices only) |
assigned | Device is assigned but not yet ready |
ready | Device is connected and accepting commands |
disconnected | Connection lost -- Portal app may be closed or phone lost network |
terminated | Device has been shut down (cloud devices only) |
maintenance | Device is undergoing maintenance (cloud devices only) |
migrating | Device is being moved between hosts (cloud devices only, typically 1–5 min) |
unknown | Unexpected state |
List Devices
GET /devices
Query params:
state -- filter by state (array, e.g. state=ready&state=assigned)
type -- dedicated_physical_device, dedicated_premium_device
name -- filter by device name (partial match)
page (default: 1), pageSize (default: 20)
orderBy -- id, createdAt, updatedAt, assignedAt (default: createdAt)
orderByDirection -- asc, desc (default: desc)
Response: { items: DeviceInfo[], pagination: Meta }
Get Device Info
GET /devices/{deviceId}
Returns device details including state, stateMessage, type, and more.
Get Device Count
GET /devices/count
Returns a map of device types to counts.
Provision a Cloud Device
Cloud devices require an active subscription. If the user's plan doesn't support it, the API will return a 403 error -- inform the user they need to terminate an existing device or upgrade at https://cloud.mobilerun.ai/billing. See references/setup-and-billing.md for plan details.
Proxy required (Cloud & Physical only): Cloud Phones and Physical Phones need a proxy for internet access. The user must have at least one proxy configured before provisioning (see Proxy Configs). Personal Phones use the phone's own network and do not need a proxy.
POST /devices
Content-Type: application/json
{
"name": "my-device",
"apps": ["com.example.app"]
}
Query param:
deviceType -- dedicated_physical_device, dedicated_premium_device
After provisioning, wait for it to become ready:
GET /devices/{deviceId}/wait
This blocks until the device state transitions to ready.
Cloud device workflow:
POST /devices?deviceType=dedicated_premium_device -- provision, returns device in creating state
GET /devices/{deviceId}/wait -- blocks until ready
- Use the
deviceId for phone control or tasks
Temporary device for a task:
When the user wants to run a task but has no ready device, provision a temporary cloud device, run the task on it, then clean up:
POST /devices?deviceType=dedicated_premium_device with {"name": "temp-task-device", "apps": [...]} -- include any apps the task needs
GET /devices/{deviceId}/wait -- wait until ready
POST /tasks with the new deviceId -- run the task (or submit multiple tasks -- they will queue and execute in order)
- Monitor via
GET /tasks/{taskId}/status until all tasks finish
DELETE /devices/{deviceId} -- terminate the device
Important: DELETE /devices/{deviceId} terminates the device immediately. If any tasks are still queued or running on that device, they will be orphaned or interrupted. Always wait for all tasks to finish before terminating.
Terminate a Cloud Device
DELETE /devices/{deviceId}
Content-Type: application/json
{}
Personal devices cannot be terminated via the API. They disconnect when the Portal app is closed.
Get Device Time
GET /devices/{deviceId}/time
Returns the current time on the device as a string.
Get Device Timezone
GET /devices/{deviceId}/timezone
Returns the current timezone of the device (e.g. America/New_York).
Set Device Timezone
POST /devices/{deviceId}/timezone
Content-Type: application/json
{ "timezone": "Europe/Berlin" }
Sets the device timezone. Use standard IANA timezone identifiers.
Rename a Device
PUT /devices/{deviceId}/name
Content-Type: application/json
{ "name": "my-test-phone" }
Reboot a Device
Physical Phones and Personal Phones only. Not available on Cloud Phones.
POST /devices/{deviceId}/reboot
Reboots the device. The device will be temporarily unavailable during reboot.
Screen Observation
Take Screenshot
GET /devices/{deviceId}/screenshot
Query param: hideOverlay (default: false)
Returns a PNG image as binary data. Use this to see what's currently displayed on screen.
Get UI State (Accessibility Tree)
GET /devices/{deviceId}/ui-state
Query param: filter (default: false) -- set to true to filter out non-interactive elements.
Returns an AndroidState object with three sections:
phone_state
{
"keyboardVisible": false,
"packageName": "app.lawnchair",
"currentApp": "Lawnchair",
"isEditable": false,
"focusedElement": {
"className": "string",
"resourceId": "string",
"text": "string"
}
}
currentApp -- human-readable name of the foreground app
packageName -- Android package name of the foreground app
keyboardVisible -- whether the soft keyboard is showing
isEditable -- whether the currently focused element accepts text input
focusedElement -- details about the focused UI element (if any)
device_context
{
"screen_bounds": { "width": 720, "height": 1616 },
"display_metrics": {
"density": 1.75,
"densityDpi": 280,
"scaledDensity": 1.75,
"widthPixels": 720,
"heightPixels": 1616
}
}
screen_bounds -- the actual screen resolution in pixels. All tap/swipe coordinates use this coordinate space.
display_metrics -- physical display properties (density, DPI)
a11y_tree (Accessibility Tree)
A recursive tree of UI elements. Each node has:
{
"className": "android.widget.TextView",
"packageName": "app.lawnchair",
"resourceId": "app.lawnchair:id/search_container",
"text": "Search",
"contentDescription": "",
"boundsInScreen": { "left": 48, "top": 1420, "right": 671, "bottom": 1532 },
"isClickable": true,
"isLongClickable": false,
"isEditable": false,
"isScrollable": false,
"isEnabled": true,
"isVisibleToUser": true,
"isCheckable": false,
"isChecked": false,
"isFocusable": false,
"isFocused": false,
"isSelected": false,
"isPassword": false,
"hint": "",
"childCount": 0,
"children": []
}
Key node fields:
text -- the visible text on the element
contentDescription -- accessibility label (useful when text is empty, e.g. icon buttons)
resourceId -- Android resource ID (e.g. com.app:id/button_ok) -- useful for identifying elements
boundsInScreen -- pixel coordinates as {left, top, right, bottom}. To tap an element, calculate its center: x = (left + right) / 2, y = (top + bottom) / 2
isClickable -- whether the element responds to taps
isEditable -- whether the element is a text input field
isScrollable -- whether the element supports scrolling (swipe gestures)
children -- nested child elements (the tree is recursive)
Other boolean fields (isEnabled, isVisibleToUser, isCheckable, isChecked, isFocused, isSelected, isPassword) are available but rarely needed.
Example: reading a home screen
FrameLayout (0,0,720,1616)
ScrollView (0,0,720,1616) [scrollable]
FrameLayout (14,113,706,326)
LinearLayout (42,128,706,310) [clickable]
TextView (42,156,706,198) "Tap to set up"
View (0,94,720,1574) "Home"
TextView (14,1222,187,1422) "Phone" [clickable]
TextView (187,1222,360,1422) "Contacts" [clickable]
TextView (360,1222,533,1422) "Files" [clickable]
TextView (533,1222,706,1422) "Chrome" [clickable]
FrameLayout (48,1420,671,1532) "Search" [clickable]
To tap "Chrome": bounds are (533,1222,706,1422), so tap at x=(533+706)/2=619, y=(1222+1422)/2=1322.
Use filter=true for a cleaner tree focused on actionable elements (filters out non-interactive containers).
Never dump the full UI state with jq '.' — the a11y tree is deeply nested and can be tens of thousands of lines on screens with lists, grids, or feeds. Output will be truncated from the beginning, and you will silently lose data without knowing it.
Always use targeted jq filters:
curl -s "https://api.mobilerun.ai/v1/devices/{id}/ui-state?filter=true" \
-H "Authorization: Bearer $MOBILERUN_API_KEY" | \
jq '[.a11y_tree | recurse(.children[]?) |
select(.isClickable==true or .isEditable==true) |
{text: (.text // .contentDescription), bounds: .boundsInScreen, isEditable}]'
curl -s "https://api.mobilerun.ai/v1/devices/{id}/ui-state?filter=true" \
-H "Authorization: Bearer $MOBILERUN_API_KEY" | \
jq '[.a11y_tree | recurse(.children[]?) |
select(.contentDescription != null and .contentDescription != "") |
{desc: .contentDescription, bounds: .boundsInScreen, isClickable}]'
curl -s "https://api.mobilerun.ai/v1/devices/{id}/ui-state?filter=true" \
-H "Authorization: Bearer $MOBILERUN_API_KEY" | \
jq '[.a11y_tree | recurse(.children[]?) |
select(.isEditable==true) | {text, resourceId, bounds: .boundsInScreen}]'
curl -s "https://api.mobilerun.ai/v1/devices/{id}/ui-state?filter=true" \
-H "Authorization: Bearer $MOBILERUN_API_KEY" | \
jq '{phone_state, device_context}'
Truncation detection: If a ui-state response does not begin with { or appears to start mid-JSON, the output was truncated. Do not attempt to infer or reconstruct missing data — re-fetch with a targeted jq filter.
After extracting elements, sanity-check the result:
- Count — does the number of items make sense for what's on screen? A 3-column grid with 4 visible rows should yield ~12 items, not 8.
- Bounds continuity — items in a grid should tile from x=0. If your first item starts at x=361 instead of x=0, you are missing the left column.
- Sort order — if items should be chronological, verify dates are sequential. A gap or jump suggests missing entries.
If the count or bounds look wrong, re-fetch with a more specific filter before proceeding.
Device Actions
All action endpoints take a deviceId path parameter.
Tap
POST /devices/{deviceId}/tap
Content-Type: application/json
{ "x": 540, "y": 960 }
Taps at pixel coordinates. Use the screen_bounds from UI state and element bounds from the a11y tree to calculate where to tap.
Swipe
POST /devices/{deviceId}/swipe
Content-Type: application/json
{
"startX": 540,
"startY": 1200,
"endX": 540,
"endY": 400,
"duration": 300
}
duration is in milliseconds (minimum: 10). Common patterns:
- Scroll down: swipe from bottom to top (high startY -> low endY)
- Scroll up: swipe from top to bottom
- Swipe left/right: adjust X coordinates, keep Y similar
Global Actions
POST /devices/{deviceId}/global
Content-Type: application/json
{ "action": 2 }
| Action code | Button |
|---|
1 | BACK |
2 | HOME |
3 | RECENT |
Type Text
POST /devices/{deviceId}/keyboard
Content-Type: application/json
{ "text": "مرحبا بالعالم", "clear": false }
Types text into the currently focused input field.
clear: true -- clears the field before typing
- Make sure an input field is focused first (check
phone_state.isEditable)
- If the keyboard isn't visible, you may need to tap on an input field first
- Supports any Unicode text -- input is injected via the accessibility service, no special keyboard setup needed. When using the Tasks API (agent), standard languages work reliably but rare scripts (e.g. Cuneiform, hieroglyphs) may be affected by LLM token handling or keyboard autocomplete.
Press Key
PUT /devices/{deviceId}/keyboard
Content-Type: application/json
{ "key": 66 }
Sends an Android keycode. Only text-input-related keycodes are supported.
| Keycode | Key |
|---|
4 | BACK |
61 | TAB |
66 | ENTER |
67 | DEL (backspace) |
112 | FORWARD_DEL (delete) |
For system navigation (home, back, recent), use POST /devices/{id}/global instead.
Clear Input
DELETE /devices/{deviceId}/keyboard
Clears the currently focused input field.
Set Location
POST /devices/{deviceId}/location
Content-Type: application/json
{ "latitude": 37.7749, "longitude": -122.4194 }
Sets a mock GPS location on the device. Both latitude and longitude are required.
Get Location
GET /devices/{deviceId}/location
Returns the current GPS coordinates of the device.
Overlay Visibility
GET /devices/{deviceId}/overlay
Returns whether the Mobilerun overlay is currently visible on the device screen.
POST /devices/{deviceId}/overlay
Content-Type: application/json
{ "visible": false }
File Transfer
The path query param specifies the file path on the device. The correct path depends on device type — physical devices use /sdcard/Download/, cloud and premium devices use / as root. Try /sdcard first, fall back to / if it fails.
GET /devices/{deviceId}/files?path=/sdcard/Download
Lists files at the specified path.
POST /devices/{deviceId}/files?path=/sdcard/Download/myfile.txt
Uploads a file to the specified path. Send the file content as the request body.
GET /devices/{deviceId}/files/download?path=/sdcard/Download/myfile.txt
Downloads a file from the device. Returns binary data.
DELETE /devices/{deviceId}/files?path=/sdcard/Download/myfile.txt
Deletes a file at the specified path.
Connect Proxy to Device
Routes the device's network traffic through a proxy. For managing saved proxy configurations, see Proxy Configs.
GET /devices/{deviceId}/proxy
Returns whether a proxy is currently connected on this device.
POST /devices/{deviceId}/proxy
Content-Type: application/json
{
"host": "proxy.example.com",
"port": 1080,
"user": "username",
"password": "password"
}
Connects a proxy to the device. Supports SOCKS5 and WireGuard:
- For SOCKS5: provide
host, port, user, password, and optionally a socks5 config object
- For WireGuard: provide
wireguard with the tunnel configuration file content
name -- optional proxy name (used for WireGuard tunnel name)
smartIp -- optional, auto-detect proxy IP
DELETE /devices/{deviceId}/proxy
Disconnects any active proxy from the device.
eSIM
Physical Phones and Personal Phones only. Not available on Cloud Phones.
Important: All Physical Phones are currently hosted in Germany. The user's eSIM must support activation or roaming in Germany to work on these devices.
Manage eSIM subscriptions on devices. Requires an eSIM provider — you need to download an eSIM profile first before list/status/APN endpoints return data.
GET /devices/{deviceId}/esim
Lists all eSIM subscriptions on the device.
POST /devices/{deviceId}/esim
Content-Type: application/json
{
"smDpAddr": "smdp.example.com",
"matchingId": "ABCD-1234-EFGH",
"enable": true
}
Downloads an eSIM profile and optionally enables it. All three fields are required.
PUT /devices/{deviceId}/esim
Content-Type: application/json
{ "subId": 2 }
Enables a previously downloaded eSIM subscription by its subscription ID.
DELETE /devices/{deviceId}/esim?subId=2
Deletes an eSIM subscription. subId is a required query param.
GET /devices/{deviceId}/esim/status
Returns current eSIM connectivity information.
GET /devices/{deviceId}/esim/apn
Lists APN configurations for active eSIM subscriptions.
POST /devices/{deviceId}/esim/apn
Content-Type: application/json
{
"name": "My APN",
"apn": "internet",
"mcc": "310",
"mnc": "260",
"protocol": "IPV4V6",
"roamingProtocol": "IPV4V6",
"type": "default,supl",
"subId": 2
}
Creates and sets an APN. All fields are required.
PUT /devices/{deviceId}/esim/apn
Content-Type: application/json
{ "apnId": 1, "subId": 2 }
Selects an existing APN as preferred.
PUT /devices/{deviceId}/esim/roaming
Content-Type: application/json
{ "enabled": true }
Toggles eSIM data roaming.
App Management
List Installed Apps
GET /devices/{deviceId}/apps
Query param: includeSystemApps (default: false)
Returns an array of AppInfo:
{
"packageName": "com.example.app",
"label": "Example App",
"versionName": "1.2.3",
"versionCode": 123,
"isSystemApp": false
}
List Package Names
GET /devices/{deviceId}/packages
Query param: includeSystemPackages (default: false)
Returns a string array of package names. Lighter than the full app list.
Install App
POST /devices/{deviceId}/apps
Content-Type: application/json
{ "packageName": "com.example.app" }
Installs an app from the Mobilerun app library (not the Play Store directly).
Takes a couple of minutes and there's no status endpoint -- you'd have to poll GET /devices/{id}/apps to confirm.
Prefer manually installing via Play Store instead. Open the Play Store app on the device, search for the app, and tap install -- this is faster and more reliable. Only use this API endpoint if the user explicitly asks for it.
On personal devices, this endpoint may fail because Android blocks app installations from unknown sources by default.
Start App
PUT /devices/{deviceId}/apps/{packageName}
Content-Type: application/json
{}
Optional body: { "activity": "com.example.app.MainActivity" } -- to launch a specific activity.
Usually omitting activity is fine; it launches the default/main activity.
Stop App
PATCH /devices/{deviceId}/apps/{packageName}
Content-Type: application/json
{}
Uninstall App
DELETE /devices/{deviceId}/apps/{packageName}
Content-Type: application/json
{}
App Library (Upload & Manage APKs)
The app library stores APKs that can be pre-installed on cloud devices. Only one app per package name is allowed -- to update an app, delete the existing one first, then re-upload.
List Apps in Library
GET /apps
Query params:
page (default: 1), pageSize (default: 10)
source -- all, uploaded, store, queued (default: all)
query -- search by name
sortBy -- createdAt, name (default: createdAt)
order -- asc, desc (default: desc)
Get App by ID
GET /apps/{id}
Upload an APK
Uploading is a 3-step process:
Step 1: Create signed upload URL
POST /apps/create-signed-upload-url
Content-Type: application/json
{
"displayName": "My App",
"packageName": "com.example.myapp",
"versionName": "1.0.0",
"versionCode": 1,
"targetSdk": 34,
"sizeBytes": 5242880,
"files": [
{ "fileName": "base.apk", "contentType": "application/vnd.android.package-archive" }
],
"country": "US"
}
Required: displayName, packageName, versionName, versionCode, targetSdk, sizeBytes, files
Optional: description, iconURL, developerName, categoryName, ratingScore, ratingCount
Returns the app id and pre-signed R2 upload URLs for each file.
Step 2: Upload the APK file(s)
Upload each file directly to its pre-signed R2 URL using a PUT request.
Step 3: Confirm the upload
POST /apps/{id}/confirm-upload
Verifies the file exists in R2 and sets the app status to available.
If the upload failed, mark it:
POST /apps/{id}/mark-failed
Delete an App
DELETE /apps/{id}
Removes the app from R2 storage and the database. Use this before re-uploading an app with the same package name.
Re-uploading an App
Only one app per package name is allowed. To update:
- Find the existing app:
GET /apps?query=com.example.myapp
- Delete it:
DELETE /apps/{id}
- Upload the new version using the 3-step upload flow above
Tasks (AI Agent)
Instead of controlling a phone step-by-step, you can submit a natural language goal and let Mobilerun's AI agent execute it autonomously on the device with its own screen analysis, observe-act loop, and error recovery.
Tasks require a paid subscription with credits. If the user doesn't have an active plan, the API will return an error -- let them know they need a subscription at https://cloud.mobilerun.ai/billing. See references/setup-and-billing.md for plan and credit details.
Run a Task
POST /tasks
Content-Type: application/json
{
"task": "Open Chrome and search for weather",
"deviceId": "uuid-of-device",
"llmModel": "google/gemini-3.1-flash-lite-preview"
}
Required fields:
task -- natural language description of what to do (min 1 char)
deviceId -- UUID of the device to run on. Must be a device in ready state.
Optional fields:
llmModel -- which model to use (default: google/gemini-3.1-flash-lite-preview, see GET /models for available models). Models change frequently -- always check GET /models for the current list.
apps -- list of app package names to pre-install
credentials -- list of { packageName, credentialNames[] } for app logins
maxSteps -- max agent steps (default: 100)
reasoning -- enable reasoning/thinking (default: true). Always set to false unless the user explicitly requests it.
vision -- enable vision/screenshot analysis (default: false)
temperature -- LLM temperature (default: 0.5)
executionTimeout -- timeout in seconds (default: 1000)
outputSchema -- JSON schema for structured output (nullable). Only use when the user explicitly asks for structured/formatted data. When set, the agent returns its result as a JSON object matching the schema in the task's output field.
vpnCountry -- route through VPN in a specific country: US, BR, FR, DE, IN, JP, KR, ZA. Only use if the task specifically requires a certain region. VPN adds latency -- avoid unless needed.
continueOnFailure -- if true, this task stays queued even if the previous task on the same device fails (default: false). See Task Queuing.
Returns:
{
"id": "uuid",
"status": "queued",
"streamUrl": "string | null"
}
The response will almost always return status: "queued" -- even if the device is idle -- because promotion happens asynchronously after the response. This is normal. The task will transition to created and then running shortly after. Check GET /tasks/{id}/status to see the current state.
streamUrl -- device stream URL, or null when the task is queued (populated once the task is promoted and assigned to the device)
Writing Task Prompts
You don't see the phone screen -- the agent on the device does. Write prompts that describe what to achieve, not how to navigate the UI. The on-device agent will figure out the taps, swipes, and navigation itself.
Don't assume the UI -- describe the goal:
- Bad:
"Tap the three dots menu in the top right, then tap Settings, scroll down and tap the Dark Mode toggle"
- Good:
"Open Settings in the Chrome app and enable Dark Mode"
- You don't know what the screen looks like. The on-device agent can see it -- let it handle the navigation.
Don't pass your own UI observations into a task instruction. If you've already read the ui-state and identified an exact element, tap it directly — don't describe it to the agent. If you're submitting a task, describe the goal and let the agent do its own observation. Mixing the two — observing yourself, then handing off a screen-state description — means the agent acts on your interpretation of the screen rather than its own, and any error in your observation (truncated data, wrong coordinates) gets baked in silently.
Be specific about the important details:
- Name the exact app (not "the browser" -- say "Chrome")
- Specify exact text to type or send
- Say what counts as success
- Name the person, contact, or item to find
Single-task examples (each of these is small enough to be one task):
"Open the Settings app, go to Display, and enable Dark Mode"
"Open WhatsApp, find the conversation with John Smith, and send: Running 10 minutes late, sorry!"
"Open Chrome, go to amazon.com, search for 'wireless headphones', and report back the name and price of the top 3 results"
"Open Spotify, go to Settings, turn off Autoplay, set Audio Quality to Very High, and disable Canvas"
Include safety conditions when appropriate:
"If the app asks for login, stop and tell me"
"If the price is over $50, don't purchase -- just report the price"
Check Task Status
GET /tasks/{task_id}/status
Use this to monitor task progress:
{
"status": "running",
"succeeded": null,
"message": null,
"output": null,
"steps": 5,
"lastResponse": { "event": "ManagerPlanEvent", "data": { ... } }
}
- While queued:
status is queued, lastResponse is null. The task is waiting for the device to become free.
- While running:
lastResponse contains the agent's latest thinking, plan, and actions. Check this to understand what the agent is doing and where it's up to.
- When finished:
status is completed or failed, message has the final answer or failure reason, succeeded is true/false, lastResponse is null.
- Statuses:
queued, created, running, paused, completed, failed, cancelled
Monitoring a Task
After creating a task, follow this pattern:
- Immediately tell the user the task ID. The initial status is almost always
queued — this is normal, not an error. The task will start shortly.
- After 5 seconds -- do the first status check. This catches quick tasks and confirms the agent started. If still
queued, check every 15-30 seconds until it transitions to running.
- After 30 seconds -- check again if still running.
- Subsequent checks -- use your judgement on the interval based on:
- Task complexity -- a simple "open Chrome" task finishes fast; a multi-app workflow takes longer, so space out checks accordingly.
- Progress -- if steps are increasing and
lastResponse is changing, the agent is working well; you can wait longer between checks. If the step count and lastResponse haven't changed, the agent may be stuck; check sooner and consider warning the user.
- Time elapsed -- the longer a task has been running successfully, the more you can trust it and wait between checks.
At each check:
- Report to the user what the agent is doing (from
lastResponse -- its current plan, thinking, what step it's on).
- Optionally take a screenshot (
GET /devices/{id}/screenshot) to show the user what's on screen.
- Optionally read the UI state (
GET /devices/{id}/ui-state) for more context.
- Give the user a meaningful update, not just "still running" -- e.g. "The agent is on step 8, currently in the Settings app looking for display options."
When the task finishes:
- Report the result (
message, succeeded, output).
- If the task failed unexpectedly, auto-submit feedback (see Feedback section).
If the agent seems stuck:
- Send a message via
POST /tasks/{id}/message to nudge it in the right direction.
- Let the user know and ask if they want to steer it or cancel.
Send Message to Task
POST /tasks/{task_id}/message
Content-Type: application/json
{ "message": "Actually, search for 'weather in London' instead" }
Send instructions to steer a running agent task. Use this to correct the agent, provide additional context, or change direction mid-task. The message is queued and delivered to the agent at the next step.
Cancel Task
POST /tasks/{task_id}/cancel
Works on both queued and running tasks. Queued tasks are cancelled instantly. Running tasks transition to cancelling and stop at the next step.
Get Task Details
GET /tasks/{task_id}
Returns the full task object including configuration, status, and trajectory.
List Tasks
GET /tasks
Query params:
status -- queued, created, running, paused, completed, failed, cancelled
orderBy -- id, createdAt, finishedAt, status (default: createdAt)
orderByDirection -- asc, desc (default: desc)
query -- search in task description (max 128 chars)
page (default: 1), pageSize (default: 20, max: 100)
Task Screenshots & UI States
GET /tasks/{task_id}/screenshots -- list all screenshot URLs
GET /tasks/{task_id}/screenshots/{index} -- get screenshot at index
GET /tasks/{task_id}/ui_states -- list all UI state URLs
GET /tasks/{task_id}/ui_states/{index} -- get UI state at index
Get Task Trajectory
GET /tasks/{task_id}/trajectory
Returns the full history of events from the task execution.
Available LLM Models
GET /models
Returns the list of models available for tasks. Default: google/gemini-3.1-flash-lite-preview.
List Tasks for a Device
GET /devices/{deviceId}/tasks
Query params: page, pageSize, orderBy, orderByDirection
Agents
List Agents
GET /agents
Returns all available agents with their default configurations. Each agent has pre-configured model and settings optimized for its domain:
{
"id": 0,
"name": "Default",
"description": "General-purpose agent for any Android task.",
"llmModel": "google/gemini-3.1-flash-lite-preview",
"reasoning": true,
"vision": false,
"maxSteps": 100
}
Use agent presets as guidance for model selection -- e.g., social media tasks may benefit from the models configured in the Instagram/TikTok/X agents.
Credentials
Manage app login credentials. Credentials are organized by package name (app) and credential name (account). When running tasks, you can pass credentials so the agent can log into apps automatically.
List All Credentials
GET /credentials
Query params: page (default: 1), pageSize (default: 10)
Returns all credentials across all packages for the authenticated user.
Initialize a Package
POST /credentials/packages
Content-Type: application/json
{ "packageName": "com.instagram.android" }
Registers a package before adding credentials to it.
List Credentials for a Package
GET /credentials/packages/{packageName}
Returns all credential names and their fields for the given package.
Create a Credential
POST /credentials/packages/{packageName}
Content-Type: application/json
Creates a new named credential with fields for a package.
Get a Credential
GET /credentials/packages/{packageName}/credentials/{credentialName}
Returns the credential and all its fields.
Delete a Credential
DELETE /credentials/packages/{packageName}/credentials/{credentialName}
Deletes the credential and all its fields.
Add a Field to a Credential
POST /credentials/packages/{packageName}/credentials/{credentialName}/fields
Content-Type: application/json
{ "fieldType": "email", "value": "user@example.com" }
Field types: email, username, password, api_token, phone_number, two_factor_secret.
Update a Credential Field
PATCH /credentials/packages/{packageName}/credentials/{credentialName}/fields/{fieldType}
Content-Type: application/json
{ "value": "new-value" }
Delete a Credential Field
DELETE /credentials/packages/{packageName}/credentials/{credentialName}/fields/{fieldType}
Proxy Configs
Manage saved SOCKS5 proxy configurations. A proxy must be configured before provisioning Cloud or Physical Phones. Only SOCKS5 protocol is supported.
List Proxy Configs
GET /proxies
Returns all saved proxy configurations for the account.
Create a Proxy Config
POST /proxies
Content-Type: application/json
{
"name": "US Residential",
"host": "proxy.example.com",
"port": 1080,
"user": "username",
"password": "password",
"protocol": "socks5"
}
All fields are required.
Delete a Proxy Config
DELETE /proxies/{proxyId}
Removes a saved proxy configuration. Does not disconnect proxies already attached to devices.
Webhooks (Hooks)
Subscribe to task events via webhooks. When events occur (task created, running, completed, failed, etc.), Mobilerun sends a POST to your URL.
List Hooks
GET /hooks
Query params: page (default: 1), pageSize (default: 20), orderBy (default: createdAt), orderByDirection (default: desc)
Get Hook
GET /hooks/{hook_id}
Subscribe to a Webhook
POST /hooks/subscribe
Content-Type: application/json
{
"targetUrl": "https://example.com/webhook",
"events": ["created", "running", "completed", "failed", "cancelled"]
}
Required: targetUrl. Optional: events (list of task events to filter), service (receiving service).
Edit a Hook
POST /hooks/{hook_id}/edit
Content-Type: application/json
{
"events": ["completed", "failed"],
"state": "active"
}
Update the events filter or state (active, disabled, deleted) of a hook.
Unsubscribe
POST /hooks/{hook_id}/unsubscribe
Permanently deletes the subscription.
Get Sample Data
GET /hooks/sample
Returns sample webhook payload data -- useful for testing and field mapping.
Perform Hook (Zapier)
POST /hooks/perform
Processes a webhook payload. Used by Zapier integration.
Feedback
Submit feedback to help improve the Mobilerun platform. This is important for identifying bugs and improving agent performance.
When to auto-submit feedback:
- When a task fails unexpectedly
- When the agent behaves incorrectly or produces wrong results
- When API errors occur that seem like platform bugs
- Include the
taskId, error details, and what happened
When the user asks to submit feedback:
- Ask for a few details (what happened, what they expected) but don't push hard
- If they don't want to elaborate, just submit with whatever details you have
POST /feedback
Content-Type: application/json
{
"title": "Task failed unexpectedly",
"feedback": "The agent got stuck on the login screen and timed out after 50 steps.",
"rating": 2,
"taskId": "uuid-of-related-task"
}
Required fields:
title -- short summary (3-100 chars)
feedback -- detailed description (10-4000 chars)
rating -- 1 to 5
Optional fields:
taskId -- UUID of a related task
| Status | Meaning |
|---|
201 | Feedback submitted |
400 | Validation error |
401 | Invalid or missing API key |
429 | Rate limited -- 15/day cap reached |
Error Handling
All API errors follow this format:
{
"title": "Unauthorized",
"status": 401,
"detail": "Invalid API key.",
"errors": []
}
| Error | Likely cause | What to do |
|---|
401 | Invalid or expired API key | Ask user to verify key at https://cloud.mobilerun.ai/api-keys |
402 on POST /tasks | Insufficient credits | User needs to add credits or upgrade plan |
403 with "limit reached" | Plan limit hit (max concurrent devices) | User needs to terminate a device or upgrade |
404 / 500 on device action | Device not found or invalid ID | Verify device ID, re-list devices |
| Empty device list | No device connected | Guide user to connect via Portal APK (see references/setup-and-billing.md) |
Device disconnected | Portal app closed or phone lost network | Ask user to check phone and reopen Portal |
Billing/plan error on POST /devices | No device subscription | Tell user to add a device at https://cloud.mobilerun.ai/billing |
| Action fails on valid device | Device may be busy, locked, or unresponsive | Try taking a screenshot first to check state |
For detailed troubleshooting of common issues (device disconnects, keyboard failures, screenshot errors, app install problems, etc.), see references/troubleshooting.md.
References
- references/setup-and-billing.md — read when: helping with first-time setup (auth, Portal APK, device connection), answering billing/pricing/credit questions, or configuring webhooks.
- references/setup-guide.md — read when: provisioning Cloud or Physical Phones. Best practices for proxy, Google account, app installation.
- references/troubleshooting.md — read when: device actions fail, API calls return unexpected errors, or the user reports phone/Portal issues.
- references/use-cases.md — read when: the user isn't sure what's possible with Mobilerun.
- references/security.md — data handling, credentials, device permissions.
- references/changelog.md — version history and notable changes.