Mobilebox
Menu
API referenceDocs pages and contents

On this page

API reference

Mobilebox is an API + MCP runtime for cloud Android phones for AI agents (Android 14, arm64): $0.01 a phone-minute, ready for a first screenshot in 19.4 s in our November 2026 benchmark. Bring your own agent: the service does not run a language model or accept a natural-language task endpoint.

Authentication

No key yet: setup.md walks an agent through it. POST /v1/signup/email {email, key_name?, via?} mails the user a 6-digit code. For a new address it also answers with api_key (shown once, stored only as a hash), key, project, claimed: false, balance_usd (a small trial), expires_at, message_for_user (tell your user this line), mcp_url and next_steps: the key works at once, for one phone at a time, and without the code the workspace closes at expires_at (7 days; its phones are deleted). An address that already has a workspace, or one waiting for its code, gets only {sent: true, next}, never a second key. POST /v1/signup/verify {email, code, key_name?, via?} confirms: with Authorization: Bearer <that key> it answers 200 claimed: true and the key keeps working; without it, 201 with a new api_key and key {id, name, hint, created_at} (keys issued before the code end), plus project, new_account, balance_usd, mcp_url and next_steps, and message while the workspace has no credit yet. Neither needs a browser. A confirmed new address gets the starter credit; a known email account gets a new key for its own project; an address that signs in with GitHub or Google answers 409 account_exists (sign in there and create the key in the dashboard). A code works once, for 10 minutes; 5 wrong tries or a newer code retire it (400 auth_failed). Too many codes: 429 with retry_after. Store the key in the user’s secret store or environment; never print it.

Send Authorization: Bearer YOUR_API_KEY on phone-control /v1/* and /mcp requests. Exception: /v1/account/keys is browser-session-only account management, not an agent API; create/list/revoke keys in the dashboard. Issued agent keys cannot become browser sessions. Browser login and API-key authentication are separate. Get your key from the account dashboard; do not send a browser session cookie as an API bearer key. Existing SDK/CLI variables remain MOBILESANDBOX_API_KEY and MOBILESANDBOX_URL for compatibility. Public product name: Mobilebox.

bash
export MSB=https://mobilebox.dev
# Load MOBILESANDBOX_API_KEY from your secret manager or shell environment.
curl --fail-with-body "$MSB/v1/account" \
  -H "Authorization: Bearer $MOBILESANDBOX_API_KEY"

JSON bodies require Content-Type: application/json. Unknown fields, malformed JSON, and wrong types fail with 400 before the requested operation. JSON bodies are limited to 256 KB. Installation accepts separate binary uploads up to 300 MiB.

Phone lifecycle

Method and pathRequestResult
POST /v1/phonesOptional name, country, city, network, platform, device, idle_timeout, max_duration, timezone, locale, location, proxy, task; optional Idempotency-Key header201 phone, possibly still creating
GET /v1/phonesNonedata array of this project’s phones
GET /v1/phones/{id}NonePhone metadata and status
POST /v1/phones/{id}/startOptional idle_timeout, max_durationStarts or waits on existing phone
POST /v1/phones/{id}/parkNo bodyShuts down, keeps apps/data/logins
DELETE /v1/phones/{id}No body204; deletes phone and data
GET /v1/phones/{id}/sessionsNoneSession timestamps, billed seconds, costs, end reasons
GET /v1/accountNoneProject id, spent_usd, balance_usd, configured price, devices; email when available in a browser session
POST /v1/account/welcomeOne of: wants (list of ios_real, android_real, android_cloud, live_view, other; optional wants_other), source (chatgpt, claude, perplexity, google, github, x, reddit, hackernews, friend, other; optional source_other), or skip: trueThe optional questions the console asks once after sign-up; answers welcome: source or done
PUT /v1/account/devicesdevices: list of android_cloud, android_real, iphone, ipadThe saved answer to "Which devices do you need?" (see below)

Create, get, and start accept wait=ready or wait=parked and timeout=0 to 55 whole seconds. Default timeout: 50. A wait timing out can still return a creating/starting phone successfully. Inspect status and poll GET on the same id until ready, failed, or your own deadline; do not create duplicates.

json
{
  "id": "ph_example",
  "object": "phone",
  "name": "smoke-test",
  "platform": "android",
  "device": "cloud",
  "status": "ready",
  "created_at": "2026-01-01T00:00:00.000Z",
  "settings": { "timezone": "Europe/Berlin", "locale": "de-DE" },
  "session": {
    "id": "ses_example",
    "started_at": "2026-01-01T00:00:00.000Z",
    "ready_at": "2026-01-01T00:00:25.000Z",
    "last_active_at": "2026-01-01T00:00:25.000Z",
    "idle_timeout": 300,
    "max_duration": 900,
    "reserved_usd": "0.150000"
  },
  "failure": null
}

This is an illustrative shape, not a recorded session. Settings reflect the configured fields. Phone status can be creating, starting, ready, parking, parked, failed, or deleted. Inspect failure.code and failure.message when failure is present; failed phones can be started again.

task is one line on what your agent is trying to do ("test the sign-up flow of my Android app"). It’s optional, helps us build what agents need, and is never shown publicly; MCP’s create_phone takes it too.

Defaults: idle_timeout 300 seconds, max_duration 900 seconds. Their ranges are 60 to 3,600 seconds and 60 to 10,800 seconds respectively. A session reserves its maximum charge against available balance; inspect the account and session endpoints for the configured rate and actual billed usage. All keys in the same project share its phones and balance. When a create needs more room, the answer says we’re scaling your workspace and links a call to raise it (see errors).

Devices

DevicePriceAsk with
Cloud Android 14 phone (arm64)$0.01 per phone-minutedefault
iPhone and iPad$0.05 per phone-minuteplatform: "ios"
Real Android device$0.03 per phone-minutedevice: "real"
Dedicated devicesfrom $79 per monththe call below

Every response says which platform and device you got, and each is billed at its own price. A notice field carries anything else worth knowing about the phone you got; errors such as setup_required are in errors.

MCP create_phone takes the same platform and device. The dashboard asks "Which devices do you need?"; PUT /v1/account/devices saves that answer. Volume over 100 devices: contact sales on the same call.

Retry a create safely

Send an Idempotency-Key header (1 to 64 printable ASCII characters with no space at either end; a UUID is safest) with POST /v1/phones to make the create safe to retry after a lost response. The MCP create_phone tool takes the same value as idempotency_key; the SDK as create({ idempotencyKey }); the CLI as create --idempotency-key.

Retry within the windowResult
Same key, same options201 with the same phone in its current state, including parked or failed. No new phone, session, reservation, boot, or limit/credit check.
Same key, other options409 idempotency_conflict; the error does not repeat your input
Same key, phone was deleted410 phone_deleted; use a new key for a new phone
Rejected create (400, 402, 429, 503)Nothing is claimed; fix the cause and retry with the same key

Keys are scoped to the project, not the API key. A key holds for 24 hours from the phone’s creation, and longer while that phone is still creating; after that the same key creates a new phone. Options are compared as the phone would run them: omitted idle_timeout, max_duration and name equal their defaults, country equals the timezone, locale and location it sets, and a null proxy equals none. The wait and timeout query parameters are not options. Later changes to the phone do not make a retry a conflict. Without the header, every POST creates a phone as before. The key covers create only; start, park and delete have no key.

HTTP removes spaces at the ends of a header value before the API sees it, so a raw REST key " abc " arrives as "abc". The SDK, CLI and MCP refuse such keys with 400 invalid_request rather than change them. The key, its phone and the reservation are saved together before the phone boots, so they survive an API crash or restart. A host power loss can lose the most recent create, along with its key and phone.

Read the screen

GET /v1/phones/{id}/observe returns app, elements, and text. Add ?format=text for a compact numbered list. GET /v1/phones/{id}/screenshot returns image/png. The phone must be ready.

text
[4] Button "Save" (540,1080)
[5] TextField "Draft" [focused] (540,865)

Illustrative lines: ref, element type, text, state, center. Observe again after a screen change; old refs may fail with stale_ref. A screenshot is useful for images, maps, or canvases absent from the accessibility tree.

Watch live

POST /v1/phones/{id}/view returns { url, expires_at, command } for a running phone. The url opens a viewer that shows the phone’s screen live with each action drawn on it: a cursor moving to the target, a ripple per tap, the path of each swipe, the text being typed (dots for password fields), and a caption per step ("Tapped "Search""), also announced to screen readers. The viewer is read-only, and watching does not count as activity on the phone. command is npx mobilebox watch "<url>", which opens the viewer in its own app window (Chrome or Edge, else the default browser); npx mobilebox watch <phone_id> asks for a link with MOBILESANDBOX_API_KEY first. The dashboard’s Phones page has a Watch button on every running phone.

The link is short-lived and grants only this phone’s view in this workspace; the access token is in the URL fragment, so it reaches no server log. Ask for a new link after expires_at. The view ends when the phone parks or is deleted.

Send actions

POST /v1/phones/{id}/actions accepts an actions array of 1 to 20 items. It returns results and a fresh screen. It stops on first failure; earlier actions stay applied. HTTP 200 can contain a failed action: inspect every results[i].ok. Do not replay an entire batch blindly.

json
{
  "actions": [
    { "type": "open_app", "package": "com.android.settings" },
    { "type": "home" }
  ]
}
json
{
  "results": [{ "type": "tap", "ok": false,
    "error": { "code": "stale_ref", "message": "Illustrative error",
      "next": "Observe again and use a current target.", "retryable": false } }],
  "screen": { "app": "com.android.settings", "elements": [], "text": "Illustrative screen" }
}

The example shows the failure envelope, not a live response. Screen app and element details are described in OpenAPI. A batch stops starting new actions after 120 seconds. Use wait_for for a screen condition; avoid long blind sleeps.

Target vocabulary

ShapeMeaning
{"text":"Save"}Exact match, then contains; optional nth starts at 1
{"ref":4}Reference from the most recent observed screen
{"id":"com.app:id/save"}Resource id
{"desc":"Navigate up"}Accessibility content description
{"x":540,"y":1080}Raw pixels; verify with a current screenshot

Action vocabulary

typeFields and limits
taptarget
long_presstarget; duration_ms 100 to 10,000, default 800
swipefrom and to as [x,y]; duration_ms 50 to 5,000, default 300
scrolldirection: up, down, left, right
scroll_totext; direction up/down (default down); max_swipes 1 to 30 (default 10)
typetext up to 2,000 chars; target optional; clear/submit default false
keykey: back, home, enter, delete, recents, tab
back / homeNo additional fields
open_app / close_apppackage; close_app backgrounds then force-stops
waitms 0 to 10,000
wait_fortext; gone default false; timeout_ms 100 to 30,000, default 10,000

Coordinates are integers from 0 to 10,000. Text targets are nonempty and up to 500 characters. Ref range: 1 to 10,000. Text nth range: 1 to 100. type needs a focused field: tap it first or pass target. type_not_applied means text did not reach it.

Network

NetworkPriceAsk with
Standard networkincludeddefault (network: "datacenter")
Residential IP: city targeting, sticky per phone$8 per GBnetwork: "residential"
Mobile carrier IP: city targeting, sticky per phone$15 per GBnetwork: "mobile"
Your own proxyincludedproxy

Add country and city to choose where, e.g. {"network": "residential", "country": "DE", "city": "Berlin"}. A city sets GPS, timezone and locale to match it (Berlin: 52.52, 13.405, Europe/Berlin, de-DE), and implies its country. Any city name works (letters, spaces and hyphens, up to 64): for one we’re still setting up, the phone uses its country’s location meanwhile and notice says so. Residential and mobile traffic is billed per GB. Your own proxy wins over network. A notice field says anything worth knowing about the network the phone is on. PATCH /v1/phones/{id}/device and MCP set_device take network, country and city too.

Acceptable use: no fake engagement, fake accounts, spam or evading platform protections (terms).

Install and manage apps

POST /v1/phones/{id}/apps accepts binary APK/.apks/.xapk bytes, or JSON {"url":"https://download.example/app.apk"}. Optional grant_permissions=true is accepted in the query or JSON install body. The response is 201 with package, version_code, and version_name. Split selection uses arm64-v8a, density, language, base and feature splits.

GET /v1/phones/{id}/apps lists user-installed packages and version_code. DELETE /v1/phones/{id}/apps/{package} uninstalls and deletes app data, returning 204. Updating retains data when the signature is compatible; signature_mismatch requires an intentional fresh-install decision, not automatic uninstall.

Configure device settings

GET /v1/phones/{id}/device reads adapter-reported settings. PATCH accepts country and city (GPS, timezone and locale together), network (see Network), timezone (IANA), locale (BCP-47), location {lat,lng}, and proxy (a supplied socks5:// URL or null for direct egress); explicit fields win over the ones a city sets. These settings persist across park/start. Changing locale restarts the UI; observe again afterward.

Country and city are shortcuts for timezone, locale, and GPS; the IP comes from the network (see Network). Proxy credentials are redacted from phone metadata; keep secrets out of your own logs and screenshots too.

Python client

pip install mobilebox (PyPI): a typed client for the REST API, Python 3.10 and newer, standard library only. Mobilebox() reads MOBILESANDBOX_API_KEY and MOBILESANDBOX_URL (default https://mobilebox.dev), waits 65 s per request and sends mobilebox-python/<version> as User-Agent; pass api_key, base_url, timeout or user_agent to change them. Quickstart example.

MethodDoesREST
create_phone(**options)Create a phone and return it ready (wait=False returns at once; idempotency_key= to retry across processes). Options are the create fields abovePOST /v1/phones
list_phones(), get_phone(id)This project’s phones, one phoneGET /v1/phones, GET /v1/phones/{id}
park_phone(id), start_phone(id)Shut down and stop billing; resume with apps and dataPOST /v1/phones/{id}/park, /start
delete_phone(id)Delete the phone and everything on itDELETE /v1/phones/{id}
observe(id)The screen: Screen(text, app, elements); screen.find(text) returns the elementGET /v1/phones/{id}/observe
act(id, actions)Run 1 to 20 actions, stop at the first failure: ActResult(results, screen)POST /v1/phones/{id}/actions
screenshot(id)PNG bytesGET /v1/phones/{id}/screenshot
install_app(id, url=None, file=None, grant_permissions=False)Install an APK, .apks or .xapk from a public URL, a path or bytesPOST /v1/phones/{id}/apps
watch(id)A read-only live view link for a personPOST /v1/phones/{id}/view
request(method, path, body)Any other endpoint, with the same auth and errorsAny

Every API error raises MobileboxError with code, message, next, retryable, retry_after and the HTTP status; do what next says before retrying. A failed action is not an exception: act returns result.ok == False and result.error, earlier actions stay applied, and you continue from result.screen. create_phone sends an Idempotency-Key (a UUID unless you pass one) and retries retryable errors with it; phone_limit and box_full are not retried.

For LangChain and LangGraph agents, pip install langchain-mobilebox (PyPI) wraps this client as tools: MobileboxToolkit().get_tools() returns seven tools (mobilebox_create_phone, mobilebox_observe, mobilebox_act, mobilebox_screenshot, mobilebox_install_app, mobilebox_park_phone, mobilebox_delete_phone), or pass client= to single tools like MobileboxObserve and MobileboxAct so your code owns the phone. Errors come back to the model as text with code and next. LangGraph example.

MCP mapping

The authenticated /mcp endpoint uses stateless Streamable HTTP. Tools: list_phones, create_phone, start_phone, park_phone, delete_phone, install_app, list_apps, uninstall_app, observe, act, screenshot, set_device, watch_phone, request_capability. request_capability {what, why?} records something the agent needed that Mobilebox doesn’t offer yet, for the team to answer. Call tools/list through a compatible client for the actual input schemas. create_phone accepts idempotency_key with the same rules as the Idempotency-Key header. MCP installation accepts a URL; use REST for local binary uploads. REST exposes account/session/device-read endpoints beyond the current tool set. Every /mcp request needs a key or an OAuth access token on that same request; without one it answers 401 with a WWW-Authenticate header that names the sign-in (see MCP sign-in). Server card: GET /mcp/server-card, listed in /.well-known/ai-catalog.json.

MCP tool errors set isError and include code, message, and next guidance in text. An act result can contain a failed action within its normal result text; inspect the full result. Keep your API key out of model-visible prose.

MCP sign-in

Clients with MCP OAuth (Claude and ChatGPT connectors, Claude Code, Cursor, VS Code) need no key: add https://mobilebox.dev/mcp and the client signs your user in. A request without credentials gets 401 with WWW-Authenticate: Bearer resource_metadata="https://mobilebox.dev/.well-known/oauth-protected-resource/mcp"; the same document is also at /.well-known/oauth-protected-resource. It names the authorization server, https://mobilebox.dev, described at /.well-known/oauth-authorization-server.

  • Registration: POST /oauth/register (RFC 7591). Every client is public (token_endpoint_auth_method none). redirect_uris: https, http on loopback (any port at sign-in) or an app scheme, no fragment.
  • Authorization: GET /oauth/authorize with response_type=code, PKCE (code_challenge_method=S256 only), state and resource=https://mobilebox.dev/mcp. Your user signs in (GitHub, Google or an email code) and chooses Allow or Cancel for one workspace. The consent page names your client by its client_name only when the redirect goes back to a known client (claude.ai, claude.com, chatgpt.com, chat.openai.com, vscode.dev, loopback, or the cursor, vscode and vscode-insiders schemes); otherwise it leads with the return host and shows client_name as what the client calls itself, not verified. The code works once, for 5 minutes; using it twice revokes what it gave.
  • Tokens: POST /oauth/token (form-encoded) with authorization_code or refresh_token. Access tokens last an hour and work on /mcp only, not REST. Refresh tokens rotate: each works once, and a used one coming back revokes the connection. POST /oauth/revoke ends the connection.
  • Your user sees connected apps under API keys in the dashboard and can revoke any of them at once.

Source: docs/REFERENCE.md in the repo, rendered at build time.