Driving a phone yourself takes two calls in a loop: observe to see it, and act to do one thing. Each call holds the phone for your key for five minutes, so a run or another key cannot take it from under you.
Endpoints
reason says why: push-timeout, checksum-mismatch or upload-gone.A phone held by a run or another key answers 409 leased. If someone takes over the phone in the console's live view, your commands fail with preempted until they let go.
Observe
screenshotobject- Include one.
maxEdgeis 240 to 2000 pixels (960 by default),formatisjpegorpng, andqualityis 30 to 95. uiTreeboolean- Include the elements on screen, up to 400. Off by default, because apps can notice it being read.
foregroundboolean- Include the app and activity in front, and whether the keyboard is up. On by default.
settleobject- Wait for the screen to stop changing first: up to
maxMs(3,000 by default) forquietMsof stillness (500 by default).
The screenshot comes back as base64 with its width and height. Send coordinates to act in that picture's pixels with its size as frame, and the phone scales them to its screen. Without a frame, they are the screen's own pixels.
Act
An action is an object with a type and the fields for it. Add then, which takes the same fields as observe, to see the result without a second call.
curl https://api.distilled.cx/v1/phones/phone-1/act \
-H "Authorization: Bearer $DISTILLED_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tap-search-0001" \
-d '{
"action": { "type": "tap", "x": 216, "y": 212 },
"frame": { "width": 432, "height": 960 },
"then": { "screenshot": {} }
}'| type | Fields | Does |
|---|---|---|
tap | x, y | Taps a point |
longPress | x, y, durationMs | Holds for 300 to 5,000 ms |
swipe | x1, y1, x2, y2, durationMs | Swipes over 80 to 5,000 ms |
type | text | Types up to 500 characters of plain ASCII |
key | key | Presses back, home, recents, enter, backspace, tab, escape, space, a direction or volume |
launchApp | package | Opens an app by its package name |
openUrl | url | Opens an http or https link in Chrome |
wait | ms | Waits up to 10 seconds |
Send an Idempotency-Key header with an action you might retry, and a repeat of the same request is not played twice.
Files
Put a video, a picture or any other file on a phone. A picture or video goes to the phone's camera roll, so its gallery and every app's media picker show it, and anything else goes to Downloads. The answer comes once the phone has the file, and says where it is and whether the gallery lists it. Unlike observe and act, this holds no lease, so it works while someone else drives the phone.
| Field | Meaning |
|---|---|
name | The file's name on the phone: letters, digits, dots, dashes and underscores, with its extension |
sha256 | Optional: what the bytes must hash to, if you want that checked |
path | In the answer: where the file is on the phone |
gallery | In the answer: whether the camera roll lists it |
The phone takes JPEG, PNG, WebP, GIF, HEIC, MP4, MOV, WebM, MKV, MP3, M4A, WAV, OGG, PDF, text, CSV, JSON and ZIP files, up to 100 MB each. Start an upload, send the file in parts of 16 MB to the address the answer names, each with its token, and finish the upload with every part's number and etag. A part that fails can simply be sent again. The parts go straight to storage, not through this API.
# Start. The answer names where to send the parts (uploadUrl), the token to send them with, and partBytes.
curl https://api.distilled.cx/v1/phones/phone-1/file-uploads \
-H "Authorization: Bearer $DISTILLED_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "long.mp4", "size": 80000000 }'
# Cut the file into parts of partBytes (16 MB) and send each straight to the file store, numbered from 1.
# Each answers { "part": 1, "etag": "..." }.
split -b 16777216 -d long.mp4 part-
n=0; for p in part-*; do n=$((n+1))
curl -X PUT "$UPLOAD_URL/parts/$n" \
-H "Authorization: Bearer $UPLOAD_TOKEN" \
--data-binary @"$p"
done
# Finish with every part's number and etag. The file is on the phone when this answers.
curl https://api.distilled.cx/v1/phones/phone-1/file-uploads/pfu_0123456789abcdef/finish \
-H "Authorization: Bearer $DISTILLED_KEY" \
-H "Content-Type: application/json" \
-d '{ "parts": [{ "part": 1, "etag": "a1b2c3" }] }'- An upload stays open until
expiresAt, an hour plus two minutes for each part, and its token is never renewed. - A workspace can have 5 open at once; a sixth answers
429 busyuntil one is finished or dropped. It can start 20 GB of uploads a day (UTC); past that a start answers429 quota-exceeded. - Every part is
partByteslong except the last. A part that failed can be sent again with the same token, in any order, and the last copy is kept. Once you finish, the upload is closed whatever the answer. - The upload's token lets in parts of that one upload and nothing else, so a web page on any site can send the parts from the browser without your API key: start and finish the upload from your server, and hand the page only
uploadUrlandtoken.
| A part answers | Meaning |
|---|---|
200 | Kept. The body has the part's part and etag. |
400 | The part is past the file's last, or isn't the length that part must be. Nothing was kept. |
401 | The token is wrong or the upload ran out of time. Start again. |
409 | The upload was finished or dropped. |
411 | The part was sent without a length. |
413 | The part is longer than partBytes. |