API reference

Drive your phones from your own code

Nine endpoints over HTTPS: create a device, boot it, run commands or a whole automation on it, and shut it down. Everything the dashboard does to a phone, your code can do — under the same plan limits and the same billing.

The downloadable file holds this entire reference plus the automation guide. Hand it to an AI assistant and it can write the integration for you.

Quick start

Keys are created in the dashboard and shown once. Send yours as a bearer token on every request.

# 1. Create a key at devicefarm.io/dashboard/api-keys
export DEVICEFARM_API_KEY="df_live_…"

# 2. List what you already have
curl https://devicefarm.io/api/v1/phones \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"

# 3. Start one, work on it, stop it
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/start \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"

What a key can — and cannot — do

The API is not a side door. It runs through the same ownership checks, the same plan cap and the same runtime budget as the dashboard, because it calls the same code underneath.

Your account, and nothing else

A key only ever resolves phones owned by the account that created it. A phone id belonging to someone else returns the same 404 as an id that never existed — the API will not even confirm it exists.

Your plan still applies

Creating a phone goes through the same cap as the dashboard. At the limit you get 403 phone_limit_reached, no matter how the request arrives.

Your minutes still apply

Starting a phone reserves the runtime it will need and sets an automatic shutdown deadline. With no minutes funded the start is refused with 409 no_budget — the API cannot run a phone you have not paid for.

Revocable at any moment

Keys are stored hashed and shown once. Revoke one in the dashboard and it stops working on the next request, with no effect on your other keys.

Conventions

Base URL

Every path below is relative to https://devicefarm.io/api/v1. Requests carrying a body are JSON.

Rate limit

60 requests per minute per account, across all your keys. Beyond that you get a 429 — the limit exists so one runaway loop cannot slow the fleet down for everyone else, including you.

Phone statuses

startingBooting. Poll until it turns running.
runningUp and billing by the minute.
stoppedOff. Costs nothing beyond the plan slot it occupies.
errorThe last boot failed. The dashboard shows why.

Response shape

Always the same envelope, so you branch on one field instead of on the status code.

// Success — data is filled, error is null
{ "data": { … }, "error": null }

// Failure — data is null, error carries a stable code
{ "data": null, "error": { "code": "phone_limit_reached", "message": "Phone limit reached (50/50)." } }

Endpoints

GET/phones200

List your phones

Every phone on the account, newest first. Deleted phones are excluded.

Query parameters
limit
integer

How many to return. Default 50, maximum 200.

Request
curl https://devicefarm.io/api/v1/phones \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
Response
{
  "data": [
    {
      "id": "3f2b9c1e-5a44-4d0b-9c1a-7e8f2d3b4a55",
      "name": "US warmup 01",
      "status": "stopped",
      "android": "Android 14",
      "brand": "Samsung",
      "model": "Galaxy A55",
      "region": "United States-US",
      "created_at": "2026-08-11T09:14:22.481Z"
    }
  ],
  "error": null
}
POST/phones201

Create a phone

Provisions a device and attaches a proxy to it. Counts against your plan the moment it succeeds. A proxy is mandatory — a phone without its own IP is a phone that shares an identity with every other one.

Body
name
string · required

Your label for the device, up to 100 characters.

deviceBrand
string · required

e.g. "Samsung". Left to us, the device would advertise a chipset it does not render with.

deviceModel
string · required

e.g. "Galaxy A55". Only pairs verified against the real GPU are accepted — a rejected pair comes back with the full accepted list for that Android version.

androidVersion
string

Android 10 through Android 16. Defaults to Android 14.

proxy
object · required

{"mode":"custom","type":"socks5"|"http"|"https","host":"…","port":1080,"username":"…","password":"…"} or {"mode":"saved","proxyId":"<uuid from the dashboard>"}.

datacenter
string

auto, sgp, us or cn. Where the device physically runs — not its internet IP, which comes from the proxy. us hosts Android 15 only.

region
string

Mobile region shown by the device, e.g. "United States-US".

language
string

Full entry from the language list, e.g. "English (United States)_en-us".

netType
string

wifi or mobile — the connection type the device reports. Not supported on Android 14.

tags
string[]

Up to 20 tags for your own filtering.

group
string

Group name used to organise the fleet.

note
string

Free text, up to 1500 characters.

Request
curl -X POST https://devicefarm.io/api/v1/phones \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "US warmup 01",
    "deviceBrand": "Samsung",
    "deviceModel": "Galaxy A55",
    "androidVersion": "Android 14",
    "proxy": {
      "mode": "custom",
      "type": "socks5",
      "host": "gate.example-proxies.net",
      "port": 1080,
      "username": "user-session-01",
      "password": "••••••"
    }
  }'
Response
{
  "data": {
    "id": "3f2b9c1e-5a44-4d0b-9c1a-7e8f2d3b4a55",
    "name": "US warmup 01",
    "status": "stopped"
  },
  "error": null
}
invalid_body
HTTP 400

A field is missing or invalid. The message names the field and, for a rejected device, lists every accepted brand/model pair.

phone_limit_reached
HTTP 403

You already hold as many phones as your plan allows. The message shows usage and limit.

create_failed
HTTP 502

Provisioning failed. Nothing is charged and no phone is left behind.

POST/phones/{id}/start202

Start a phone

Boots the device and reserves the runtime minutes it will need. Returns 202 immediately — booting takes tens of seconds, so poll the list until status is running. Runtime bills by the minute from this call until the phone stops.

Request
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/start \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
Response
{
  "data": { "id": "3f2b9c1e-…", "status": "starting" },
  "error": null
}
phone_not_found
HTTP 404

No phone with that id on your account.

no_budget
HTTP 409

Not enough runtime minutes for this boot. Either the balance is empty, or what is left cannot fund another phone alongside the ones already running — stop one, or buy a time add-on. The message says which.

trial_exhausted
HTTP 409

Free trial minutes used up.

trial_max_running
HTTP 409

Trial accounts run one phone at a time.

upstream_busy
HTTP 503

Transient capacity limit. Retry in a few seconds.

start_failed
HTTP 502

The device did not boot. Nothing is billed; retry.

POST/phones/{id}/stop202

Stop a phone

Shuts the device down and settles the minutes used. Safe to call on a phone that is already stopped. Call it as soon as your work is done — an idle running phone bills exactly like a busy one.

Request
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/stop \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
Response
{
  "data": { "id": "3f2b9c1e-…", "status": "stopped" },
  "error": null
}
upstream_busy
HTTP 503

Transient capacity limit. Retry in a few seconds.

stop_failed
HTTP 502

The device did not stop. Retry; if it persists the idle reaper will stop it.

POST/phones/{id}/prepare200

Install the automation library

Writes the human-input library to /data/local/tmp/humanize.sh on a running phone. Every script below opens by sourcing that file, so call this once per boot before running anything. Idempotent. Pass the packages you are about to automate and the accessibility flag is masked for them.

Body
packages
string[]

Up to 20 package names, e.g. ["com.instagram.android"]. Masking is not available on Android 16 — the response still echoes the list, so read it as "requested", not "confirmed", on that version.

Request
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/prepare \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"packages": ["com.instagram.android"]}'
Response
{
  "data": {
    "id": "3f2b9c1e-…",
    "library": "/data/local/tmp/humanize.sh",
    "bytes": 9214,
    "accessibility_hidden": ["com.instagram.android"]
  },
  "error": null
}
invalid_body
HTTP 400

Expected {"packages": ["com.example"]} or an empty body.

phone_not_running
HTTP 409

Start the phone first.

upstream_busy
HTTP 503

Transient capacity limit. Retry in a few seconds.

prepare_failed
HTTP 502

The library could not be installed on the device.

POST/phones/{id}/shell200

Run a command

Runs one command on the device as the shell user. No ADB, no credentials to store. A command that exits non-zero is a result, not an API error — you get its output either way. Times out after about ten seconds; use a script for anything longer.

Body
cmd
string · required

The command, up to 8192 characters.

Request
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/shell \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"cmd": ". /data/local/tmp/humanize.sh && h_tap 540 1200"}'
Response
{
  "data": { "ok": true, "output": "" },
  "error": null
}
invalid_body
HTTP 400

Expected {"cmd": "<command>"}.

phone_not_running
HTTP 409

Start the phone first.

upstream_busy
HTTP 503

Transient capacity limit. Retry in a few seconds.

device_unreachable
HTTP 502

The device did not respond.

POST/phones/{id}/script202

Run a script detached

Uploads a full flow and runs it in the background. Prefer this over a long chain of shell calls: one request instead of one per gesture, no ten-second ceiling, and the pacing comes from the script rather than from network latency.

Body
script
string · required

The whole shell script, up to 64 KB. Open it with `. /data/local/tmp/humanize.sh`. Every timing argument in that library is in MILLISECONDS.

stop_when_done
boolean

Default true — the phone is shut down on the poll that observes the run finishing. Keep polling until it reports phone_stopped, otherwise the device stays up and billing until the idle sweep catches it.

Request
curl -X POST https://devicefarm.io/api/v1/phones/$PHONE_ID/script \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "script": ". /data/local/tmp/humanize.sh\nam start -n com.android.settings/.Settings\nh_sleep 2000 4000\nh_swipe 540 1600 540 700\n",
    "stop_when_done": true
  }'
Response
{
  "data": {
    "phone_id": "3f2b9c1e-…",
    "run_id": "a41f9c2b7e08",
    "status": "running"
  },
  "error": null
}
invalid_body
HTTP 400

Missing script, or larger than 64 KB.

phone_not_running
HTTP 409

Start the phone first.

script_failed
HTTP 502

The script could not be launched on the device.

GET/phones/{id}/script200

Poll a run

Progress and accumulated output. `running: false` with an exit_code means it finished. Keep polling until it does: the poll that observes the end is what shuts the phone down, so stopping early leaves it billing.

Query parameters
run_id
string · required

The run_id returned when the script was launched.

Request
curl "https://devicefarm.io/api/v1/phones/$PHONE_ID/script?run_id=$RUN_ID" \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
Response
{
  "data": {
    "phone_id": "3f2b9c1e-…",
    "run_id": "a41f9c2b7e08",
    "running": false,
    "exit_code": 0,
    "output": "Starting: Intent { cmp=com.android.settings/.Settings }",
    "phone_stopped": true
  },
  "error": null
}
invalid_run_id
HTTP 400

The run_id is missing or malformed.

status_failed
HTTP 502

The run could not be read.

DELETE/phones/{id}200

Delete a phone

Releases the device and frees the plan slot immediately. Permanent — there is no undo, exactly like the delete button in the dashboard. A running phone is stopped and its session billed first, and the accounts you created on it are kept in your dashboard.

Request
curl -X DELETE https://devicefarm.io/api/v1/phones/$PHONE_ID \
  -H "Authorization: Bearer $DEVICEFARM_API_KEY"
Response
{
  "data": { "id": "3f2b9c1e-…", "deleted": true },
  "error": null
}
delete_failed
HTTP 502

The phone could not be released. Nothing changed — retry.

Errors on any endpoint

missing_key
HTTP 401

No Authorization header, or it does not carry a df_live_ key.

invalid_key
HTTP 401

The key is unknown or has been revoked. Unknown and revoked answer identically on purpose.

rate_limited
HTTP 429

More than 60 requests in a minute on this account. Slow down and retry.

rate_unavailable
HTTP 503

The rate limiter could not be reached, so the request was refused rather than let through unmetered.

phone_not_found
HTTP 404

No phone with that id on your account. A phone belonging to someone else answers exactly the same.

Next

Skip the code entirely

The same API is exposed to Claude as a set of tools. Connect it once and you drive the fleet in plain language — create a phone, install the automation library, run a flow, stop the phone — without writing a request yourself.

Connect DeviceFarm to Claude