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
| Request | What it does |
|---|---|
GET /devices | List 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 /ws | A 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 answers415, 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}
| Status | Typical codes |
|---|---|
200 | success |
400 | BAD_ARGS, DEVICE_AMBIGUOUS |
401 | UNAUTHORIZED |
404 | UNKNOWN_CMD, DEVICE_NOT_FOUND |
409 | NO_ACCESSIBILITY, NO_IME, NO_PERMISSION |
422 | NOT_FOUND, AMBIGUOUS, MACRO_FAILED and other things the phone could not do |
503 | DEVICE_OFFLINE, AGENT_RESTARTED |
504 | TIMEOUT |
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.