HTTP API

Drive phones with plain HTTP requests from curl, PowerShell, Python requests, fetch or any no-code tool: endpoints, request bodies, replies, status codes and tokens.

The port your code connects to, localhost:8780, also speaks HTTP. That makes Droidline usable from any language or tool that can send a web request: curl, PowerShell, a no-code automation tool, or a language without an SDK. One request runs one command and returns one JSON reply.

Endpoints

RequestWhat it does
GET /devicesList paired phones
POST /devices/{device}/{command}Run a phone command. The JSON body holds its parameters.
GET /devices/{device}/screenshot.png (or .jpg)The screenshot image itself. ?scale=0.5&quality=70 also work.
POST /server/{command}Run a server command: pair, pair_qr, rename, revoke
GET /wsA WebSocket carrying the same lines as the raw socket

{device} is a phone's name or ID, or _ for "the only phone that is online". {command} is any name from the command reference, and the body uses the parameter names listed there.

A first request

curl -s -X POST localhost:8780/devices/_/touch \
  -H 'content-type: application/json' \
  -d '{"by":"text","value":"OK"}'
Invoke-RestMethod -Method Post -Uri http://localhost:8780/devices/_/touch `
  -ContentType "application/json" -Body '{"by":"text","value":"OK"}'
import requests

r = requests.post("http://localhost:8780/devices/_/touch",
                  json={"by": "text", "value": "OK"})
print(r.status_code, r.json())
const r = await fetch("http://localhost:8780/devices/_/touch", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ by: "text", value: "OK" }),
});
console.log(r.status, await r.json());

Two things every POST needs:

  • The header Content-Type: application/json. Without it the server answers 415, even for commands that take no parameters.
  • A JSON object as the body. For a command without parameters, send {} or no body at all.

Replies

A successful command answers 200 with "ok": true and its result spread into the object. Commands that return one value put it under value:

{"ok":true,"via":"node","ms":208}
{"ok":true,"value":"12,500"}
{"ok":true,"package":"dev.droidline.demo","activity":".MainActivity"}

A failed command answers with the HTTP status listed for its error code, and the same JSON shape with "ok": false:

{"ok":false,"error":"NOT_FOUND","msg":"Could not find text 'Nope' within 1s. Current screen: dev.droidline.demo / .MainActivity","retryable":true,"screen":"dev.droidline.demo/.MainActivity","target":"text 'Nope'","timeout":1}
StatusTypical codes
200success
400BAD_ARGS, DEVICE_AMBIGUOUS
401UNAUTHORIZED
404UNKNOWN_CMD, DEVICE_NOT_FOUND
409NO_ACCESSIBILITY, NO_IME, NO_PERMISSION
422NOT_FOUND, AMBIGUOUS, MACRO_FAILED and other things the phone could not do
503DEVICE_OFFLINE, AGENT_RESTARTED
504TIMEOUT

Read error and retryable from the body rather than relying on the status alone.

Common requests

# Which phones are paired, and are they online?
curl -s localhost:8780/devices

# Open an app on a named phone
curl -s -X POST localhost:8780/devices/shelf-01/launch \
  -H 'content-type: application/json' -d '{"package":"com.android.settings"}'

# Is something on the screen?
curl -s -X POST localhost:8780/devices/_/exists \
  -H 'content-type: application/json' -d '{"by":"text","value":"Close ad"}'

# Wait longer than the default
curl -s -X POST localhost:8780/devices/_/touch \
  -H 'content-type: application/json' -d '{"by":"text","value":"Done","timeout":30}'

# Save the screen tree and a screenshot
curl -s -X POST localhost:8780/devices/_/dump -H 'content-type: application/json' > screen.json
curl -s "localhost:8780/devices/_/screenshot.jpg?scale=0.5" -o shot.jpg

# Renew the mobile IP and wait for the phone to come back
curl -s -X POST localhost:8780/devices/_/batch -H 'content-type: application/json' \
  -d '{"steps":[["airplane",true],["sleep",3000],["airplane",false]],"wait":true}'
# Which phones are paired, and are they online?
(Invoke-RestMethod http://localhost:8780/devices).value | Format-Table name, online, route

# Open an app on a named phone
Invoke-RestMethod -Method Post -Uri http://localhost:8780/devices/shelf-01/launch `
  -ContentType "application/json" -Body '{"package":"com.android.settings"}'

# Is something on the screen? Build the body from a hashtable.
$body = @{ by = "text"; value = "Close ad" } | ConvertTo-Json
(Invoke-RestMethod -Method Post -Uri http://localhost:8780/devices/_/exists `
  -ContentType "application/json" -Body $body).value

# Save a screenshot
Invoke-WebRequest "http://localhost:8780/devices/_/screenshot.jpg?scale=0.5" -OutFile shot.jpg

# Catch an error and read its code
try {
  Invoke-RestMethod -Method Post -Uri http://localhost:8780/devices/_/touch `
    -ContentType "application/json" -Body '{"by":"text","value":"Nope","timeout":1}'
} catch {
  $err = $_.ErrorDetails.Message | ConvertFrom-Json
  "$($err.error): $($err.msg)"
}

Network-cutting commands

data(false), wifi(false), airplane(true) and batches containing them cut the phone's own connection. Over HTTP, always send "wait": true: the request then stays open until the phone is back and answers with the final result. Without it, you only get {"ok":true,"accepted":true}, and the final result goes to a connection that has already closed.

Tokens and other machines

By default the server only accepts connections from the same computer, and they need no token. If you open the port to your network (client.listen = "0.0.0.0:8780" in config.toml), every request needs a client token in the Authorization header:

droidline token create "office laptop"     # prints dlc_... once

curl -s -X POST http://192.168.0.12:8780/devices/_/current \
  -H 'content-type: application/json' \
  -H 'Authorization: Bearer dlc_...'

Read Security before you do this.

Calls from web pages

The server refuses requests from web pages unless their origin is listed in client.allowed_origins, so a site you happen to visit cannot drive your phones. To call the API from your own local web app, add its origin, for example allowed_origins = ["http://localhost:5173"], and restart the server.