CLI
Install the phonebox CLI, set its environment, and use every command, its output and its exit codes.
The phonebox CLI drives phones from a terminal. It suits coding agents, which already work in one, and quick checks by hand. Every command calls the REST API, so keys, limits and billing work as they do there.
Install
The CLI needs Node.js 20 or later. One command installs it and sets you up:
curl -fsSL https://phonebox.dev/install | shThe script installs the package with npm and runs phonebox setup. To install without setting up:
npm install -g https://phonebox.dev/downloads/phonebox-0.1.0.tgz
phonebox --helpRun the CLI as the installed phonebox command. The package ships only from phonebox.dev, so an npx call that doesn't name the file would fetch whatever the npm registry holds under that name, and run it with your key in its environment. For a one-off run without installing anything, name the file:
npx -y --package=https://phonebox.dev/downloads/phonebox-0.1.0.tgz phonebox --helpInside a project, npm install https://phonebox.dev/downloads/phonebox-0.1.0.tgz adds the package, which is also the TypeScript SDK. The package's SHA-256 checksum is published next to it, at https://phonebox.dev/downloads/phonebox-0.1.0.tgz.sha256.
Environment
| Variable | Meaning |
|---|---|
PHONEBOX_API_KEY | Your API key. It wins over the key that phonebox setup saved; without either, commands fail with a usage error. |
PHONEBOX_PHONE | A phone ID. Commands that act on a phone use it when you leave the ID out. |
PHONEBOX_URL | Another API origin. Leave it unset to use https://phonebox.dev. |
export PHONEBOX_PHONE=ph_7kx2m6q4v3ta
phonebox lookCommands
<id> is a phone ID, which you can leave out when PHONEBOX_PHONE is set. A first argument that starts with ph_ but isn't a valid phone ID, or an argument that the command doesn't take, is a usage error, so a mistyped ID never falls back to the phone in PHONEBOX_PHONE. Durations such as --idle, --max, --expires and --timeout take seconds or forms like 90s, 10m and 1h30m. Every command prints its own help with --help, as in phonebox tap --help.
Setup
phonebox setup [--no-wait] [--no-open] [--email] [--amount 10] [--client NAME]
phonebox login [--no-wait] [--no-open] [--email] [--client NAME]
phonebox logout
phonebox token| Command | What it does |
|---|---|
setup | Signs you in, adds credit and confirms the account, skipping what is already done. It prints a link and a code: open the link, sign in with Google, or with an emailed code when you pass --email, check that the page shows the same code, and click Connect. --client names this terminal on that page and on its key. It then prints a checkout link when the project has no credit, for $10 or --amount. On a terminal it opens each link in your browser unless you pass --no-open, and waits up to 15 minutes for you. --no-wait prints the current step and exits at once, which is how agents use it. |
login | The sign-in part of setup alone. Its last line has the step signed_in. |
logout | Forgets the saved key. The key stays valid until you revoke it under API keys. |
token | Prints the key in use, as in export PHONEBOX_API_KEY=$(phonebox token). |
Setup saves an Agent key in ~/.config/phonebox/credentials.json, or under $XDG_CONFIG_HOME, readable only by you. Its progress goes to stderr, and its last line on stdout is one JSON object whose step is sign_in, add_credits or ready:
{"step":"sign_in","url":"https://phonebox.dev/connect?code=K7QM-2XHD","user_code":"K7QM-2XHD","expires_in":900,"next":"Ask your user to open the URL, sign up or in, and confirm the code. Then run phonebox setup again."}Phones
phonebox ls [--status STATUS]
phonebox create [--name NAME] [--country CC] [--idle 10m] [--max 1h] [--idempotency-key KEY] [--no-wait]
phonebox start <id> [--idle 10m] [--max 1h] [--no-wait]
phonebox park <id> [--no-wait]
phonebox rm <id> --yes
phonebox status <id> [--wait [--timeout 10m]]
phonebox live <id> [--expires 1h]
phonebox account--country takes one of the two-letter codes that phonebox account lists under countries, such as US or DE. Leave it out for any country.
| Command | What it does |
|---|---|
ls | Lists the project's 50 newest phones, leaving out failed and deleted ones, or only those in one status with --status, such as --status failed. To see more, page through List phones with its cursor. |
create | Creates a phone and waits until it is ready, for up to 10 minutes. The moment the phone exists, before the wait, it prints the phone's ID on stderr. --no-wait prints the new phone at once instead of waiting. --idempotency-key makes a repeat of the same create return the same phone, and a repeat that finds that phone failed or deleted exits with its error. |
start | Starts a parked phone, with its apps, files and sign-ins kept, and waits until it is ready, for up to 5 minutes. On a running phone, it renews the session. |
park | Parks a phone and waits until it has stopped. Parked phones cost nothing. |
rm | Deletes a phone permanently, with its apps, files and sign-ins. It needs --yes and an Admin key. |
status | Shows a phone's status and session. --wait waits until the phone is ready, for up to 10 minutes or --timeout, only reading it, and exits with code 3 when time runs out. --timeout without --wait is a usage error. Wait this way for a phone that is still creating or starting, because start renews a running phone's session. |
live | Creates a live view link that lets a person watch and control the phone in a browser. Anyone with the link controls the phone until it expires, so share it only with your user, and never paste it into logs or shared chats. |
account | Shows your balance, limits and price. |
The screen
phonebox look <id> [--screenshot FILE.jpg] [--json]
phonebox screenshot <id> <FILE> [--png]| Command | What it does |
|---|---|
look | Prints the screen in the text form, after a first line that names its snapshot, such as snapshot snp_4f2k7m3q6z3a, which a tap by ref needs. --json prints the full observation instead, with its snapshot_id. --screenshot also saves a full-size JPEG to that file. |
screenshot | Saves a screenshot to the file, as JPEG, or as PNG with --png. |
Actions
phonebox tap <id> (X Y | --text T [--nth N] | --id R | --desc D | --ref N --snapshot S) [--long] [--double]
phonebox type <id> <TEXT> [--clear] [--submit]
phonebox key <id> <NAME|CODE>
phonebox back <id>
phonebox home <id>
phonebox recents <id>
phonebox swipe <id> X1 Y1 X2 Y2 [--ms 300]
phonebox scroll <id> <up|down|left|right> [--text T]
phonebox open <id> <PACKAGE|URL>
phonebox wait <id> --text T [--gone] [--timeout 20s]
phonebox act <id> <JSON|->| Command | What it does |
|---|---|
tap | Taps a point or an element. --long long-presses and --double double-taps. --nth goes only with --text, and the CLI refuses it with any other target as a usage error. Targets explains how each form finds its element. |
type | Types into the focused field. --clear empties it first, and --submit presses Enter afterwards. |
key | Presses a key by name, such as enter, delete, tab or escape, or by its Android key code. |
back, home, recents | Press Back, go to the home screen, or open the recent apps. |
swipe | Swipes between two points, in --ms milliseconds. |
scroll | Scrolls the screen, or the element with the text --text. |
open | Opens an app by its package name, or a URL or deep link when the argument has a scheme such as https:. |
wait | Waits until the text appears, or with --gone until it disappears. --timeout can be up to 30 seconds, and the default is 10. It exits with code 3 when time runs out. |
act | Runs a batch of actions, given as JSON or read from stdin with -. The JSON is either {"actions": […], "observe": …} or just the array of actions. |
Each of these commands but act sends one action with "observe": "none" and prints its result. act prints the whole batch result and exits with code 0 even when an action failed, so check completed and each result's ok.
echo '[{"type": "tap", "target": {"text": "Sign in"}}, {"type": "wait_for", "target": {"text": "Welcome"}}]' | phonebox act -Apps and files
phonebox apps <id> [install PACKAGE | install ./app.apk [--replace] | rm PACKAGE | installs]
phonebox uploads [ls | rm UPLOAD]
phonebox files <id> ls PATH | push LOCAL REMOTE | pull REMOTE LOCAL | rm PATH| Command | What it does |
|---|---|
apps | Lists the installed apps. apps install ./app.apk installs your own APK (an argument ending in .apk is always a file, and one that isn't there is a usage error): it uploads the file, waits while Phonebox reads it, installs it and waits until the phone lists the app, printing each step on stderr. --replace uninstalls the app first, which deletes its data: use it for a build signed with another key, or an older versionCode. apps install PACKAGE starts an install from Phonebox's app library, which never reaches Google Play, apps rm PACKAGE uninstalls an app, and apps installs lists the recent installs, each running, succeeded or failed. |
uploads | Lists the project's uploads of your own APKs, or deletes one with rm. An upload is deleted 7 days after its last install. |
files | Lists a directory, uploads a local file (push), downloads a file (pull) or deletes one, under /sdcard. When the push target ends with /, the file keeps its name. |
The device and recordings
phonebox device <id>
phonebox location <id> [LAT LNG [--keep-timezone] | reset]
phonebox reboot <id> [--no-wait]
phonebox reset <id> --yes [--no-wait]
phonebox record <id> start [--name NAME] | stop REC | ls | pull REC FILE.mp4 | rm REC
phonebox library [QUERY] [--cursor N]| Command | What it does |
|---|---|
device | Shows the phone's brand, model, Android version, screen size and carrier, with its location, locale and timezone. |
location | Shows the phone's location. With LAT LNG, sets it, and the timezone found there unless --keep-timezone is given. location <id> reset goes back to the default location and its timezone. |
reboot | Restarts a stuck phone, keeping everything on it, and waits until it is ready, for up to 5 minutes. A phone can reboot once every 10 minutes. |
reset | Wipes the phone's apps, files, signed-in accounts and clipboard, keeping its ID, and waits until it is ready. It can't be undone: it needs --yes and an Admin key. |
record | Records the phone's screen: start begins a recording and prints it with its rec_ ID, stop ends one, ls lists the phone's recordings, pull saves a ready recording's video to an MP4 file, and rm deletes one. Recordings are deleted 7 days after they start. |
library | Searches Phonebox's app library, the store and system apps that apps <id> install PACKAGE installs. --cursor reads the next page, from next_cursor. |
Output
- A command that succeeds prints one line of JSON on stdout: the phone, the action's result, the list, and so on.
- Each line on stderr is one JSON value. A
createthat is about to wait prints{"created":"ph_…","status":"creating"}there first, so stderr can hold that line and then an error.apps install ./app.apkprints{"uploading":…},{"processing":"upl_…"},{"ready":"upl_…"}and{"installing":…}there as it goes. lookprints its snapshot line and the text form instead, unless you add--json.setupandloginprint their progress as plain sentences on stderr, andtokenprints the key alone.- A command that fails prints
{"error": {…}}on stderr, with the fieldsstatus,code,message,retryable,next,request_idanddetails.statusis the HTTP status, and a usage error has the codeusage. - When the answer isn't Phonebox's own error, such as a gateway's
504, or the API can't be reached or doesn't answer in time, the code isinternal_error. After a command that changes the phone, such astaporact, look before you run it again, because it may have run.
{"error":{"status":422,"code":"ambiguous_target","message":"Several elements on the screen match the target.","retryable":false,"next":"Add nth to the target, or tap by ref from the returned observation.","request_id":null,"details":{"candidates":[{"ref":3,"type":"Button","text":"Add to cart","desc":null,"id":"com.example.shop:id/add_to_cart","center":[860,620]},{"ref":5,"type":"Button","text":"Add to cart","desc":null,"id":"com.example.shop:id/add_to_cart","center":[860,900]},{"ref":7,"type":"Button","text":"Add to cart","desc":null,"id":"com.example.shop:id/add_to_cart","center":[860,1180]}],"snapshot_id":"snp_4f2k7m3q6z3a"}}}The CLI waits for phones the way the API asks: create, start, park and status --wait poll in requests of about 50 seconds each until the phone gets there. A wait ends early with the phone's own failure when the phone can't get there, for example when a start fails and the phone parks again.
Creating phones safely
A new phone starts billing as soon as it exists, which is before it is ready. When create is going to wait, it first prints the new phone's ID on stderr:
{"created":"ph_7kx2m6q4v3ta","status":"creating"}The line appears only for a phone on its way to ready, whose status is creating or starting. If the wait is cut short, for example because your shell stops the command, the phone still exists: keep that ID, and phonebox status with it and --wait waits until the phone is ready.
An error from a create tells you whether a phone was made:
- Phonebox's own error that names no phone in
details.phone, such asinsufficient_credits,running_limit_reachedor acapacity_unavailablethat names no phone, is a refusal when nocreatedline came before it: it created nothing, and you can send the create again. internal_error, which the CLI also prints when no answer came from Phonebox, such as after a timeout, a dropped connection or a gateway's504, says nothing about the create, and neither does a stopped shell. The phone may exist, so runphonebox lsand use the newest phone with the name you gave it before you create another.- A phone is gone only when its
statusisfailedordeleted, so read the error'sretryableand the phone's status, neverdetails.phonealone. A create whose new phone failed answersretryable: false, and a repeat that finds the phone failed or deleted exits with that phone's error. Create the next phone with a new idempotency key only then.
--idempotency-key makes a create safe to repeat. Choose one key for each phone you want, for example the task's name and the time, and keep it for as long as you may need to repeat that create:
key="signup-test-$(date +%s)"
phonebox create --name signup-test --idempotency-key "$key" --no-waitThe key must be 8 to 128 characters, letters, digits, _, ., : and -, starting with a letter or digit. The CLI refuses any other key, and an empty one, such as an unset variable, with a usage error, before it sends anything. If the command fails without a phone, run it again with the same key and the same flags within 24 hours: you get the phone the first call created, as it is now, instead of a second phone. If that phone has failed, or you deleted it, a repeat only returns it again, and create exits with its error: create the next phone with a new key.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The command succeeded. |
| 1 | The API returned an error, or an action failed. |
| 2 | The command was used wrongly, for example with a malformed phone ID, an argument the command doesn't take or a malformed duration such as --max 2hours, or there is no key: run phonebox setup. Nothing was done on the phone. |
| 3 | A wait timed out: wait ran out of time, setup waited 15 minutes for you, or a phone didn't reach its status in time. After a create or start that timed out, the phone exists, so read it with phonebox status and never create another. |
| 130 | You interrupted the command with Ctrl-C. |