droidloop is the agentic Android API for AI agents and autonomous workflows. POST Kotlin and XML sources, get back a compiled APK running on a cloud Android emulator, plus screenshots and logcat from the app process. Poll for status, drive the emulator with actions, fetch results when you're done. Authenticated with a single api_key.
Submits a source bundle and starts a paid build on the shared emulator
pool. Returns immediately with a project_id. The job is
queued=1 until a device is free, then moves to building
and finally active=1 for an interactive lease.
import requests url = "https://droidloop.dev/v1/project" api_key = "CT_TESTY" payload = { "files": { "strings.xml": "<resources>…</resources>", }, "logcat_lines": 200, "api_key": api_key, } r = requests.post(url, json=payload, timeout=60) data = r.json() print(data)
curl -sS -X POST "https://droidloop.dev/v1/project" \ -H "Content-Type: application/json" \ -d '{ "api_key": "CT_TESTY", "logcat_lines": 200, "files": { "strings.xml": "<resources>…</resources>" } }'
{
"project_id": "a1b2c3d4e5",
"active": 0,
"queued": 1,
"expires_in": 0,
"error": 0,
"error_msg": "",
"status": "queued"
}
{
"project_id": "",
"active": 0,
"queued": 0,
"expires_in": 0,
"error": 1,
"error_msg": "no free emulator available — worker not connected or all devices busy",
"status": "error"
}
Lightweight status for waiting loops. Prefer this over
/status while queued=1 — it does not return
large screenshot payloads. When active=1, use
expires_in as the remaining action window.
import requests, time pid = "a1b2c3d4e5" key = "CT_TESTY" url = f"https://droidloop.dev/v1/project/{pid}/poll" while True: st = requests.get(url, params={"api_key": key}, timeout=15).json() if st.get("error"): raise SystemExit(st) if st.get("active") == 1: print("active — expires_in=", st["expires_in"]) break if st.get("queued") == 0 and st.get("active") == 0: print("finished or failed", st) exit(1) time.sleep(2)
PID="a1b2c3d4e5" API_KEY="CT_TESTY" while true; do ST=$(curl -sS -G \ "https://droidloop.dev/v1/project/${PID}/poll" \ --data-urlencode "api_key=${API_KEY}") echo "$ST" echo "$ST" | grep -q '"active": *1' && break echo "$ST" | grep -q '"error": *1' && exit 1 sleep 2 done
Only accepted while active=1. Coordinates are device pixels,
origin top-left. Every call refreshes the lease.
import requests url = "https://droidloop.dev/v1/project/action" key, pid = "CT_TESTY", "a1b2c3d4e5" # Tap requests.post(url, json={ "project_id": pid, "api_key": key, "action": "tap", "coords": [360, 800], }, timeout=15) # Screenshot requests.post(url, json={ "project_id": pid, "api_key": key, "action": "screenshot", }, timeout=15) # Swipe up requests.post(url, json={ "project_id": pid, "api_key": key, "action": "swipe", "coords": [360, 1200, 360, 400], }, timeout=15)
# Tap curl -sS -X POST "https://droidloop.dev/v1/project/action" \ -H "Content-Type: application/json" \ -d '{"project_id":"a1b2c3d4e5","api_key":"CT_TESTY","action":"tap","coords":[360,800]}' # Screenshot curl -sS -X POST "https://droidloop.dev/v1/project/action" \ -H "Content-Type: application/json" \ -d '{"project_id":"a1b2c3d4e5","api_key":"CT_TESTY","action":"screenshot"}' # Swipe up curl -sS -X POST "https://droidloop.dev/v1/project/action" \ -H "Content-Type: application/json" \ -d '{"project_id":"a1b2c3d4e5","api_key":"CT_TESTY","action":"swipe","coords":[360,1200,360,400]}'
/action are
appended to the project's screenshot list and also returned by
/v1/project/status.
Returns loot collected after a successful build: base64 PNG screenshots, logcat text, and the studio share URL. Safe to call while still queued.
import requests, base64 url = "https://droidloop.dev/v1/project/status" payload = {"project_id": "a1b2c3d4e5", "api_key": "CT_TESTY"} data = requests.post(url, json=payload, timeout=30).json() if data.get("error"): raise SystemExit(data) for i, b64 in enumerate(data.get("screenshots") or []): open(f"CT_{i}.png", "wb").write(base64.b64decode(b64)) print(data.get("project_url")) print(data.get("logcat", "")[:500]) print(data.get("project_zip_url")) print(data.get("apk_zip_url"))
STATUS=$(curl -sS -X POST "https://droidloop.dev/v1/project/status" \ -H "Content-Type: application/json" \ -d '{"project_id":"a1b2c3d4e5","api_key":"CT_TESTY"}') echo "$STATUS" | jq . echo "$STATUS" | jq -r '.screenshots[0] // empty' | base64 -d > CT_0.png ZIP=$(echo "$STATUS" | jq -r '.project_zip_url // empty') APK=$(echo "$STATUS" | jq -r '.apk_zip_url // empty') [[ -n "$ZIP" ]] && curl -sS -G "$ZIP" --data-urlencode "api_key=CT_TESTY" -o project.zip [[ -n "$APK" ]] && curl -sS -G "$APK" --data-urlencode "api_key=CT_TESTY" -o app-debug.apk
A complete runnable script: create a project, poll until active, tap the screen, take a screenshot, fetch status, save all PNGs.
import requests import time import base64 url = "https://droidloop.dev/v1/project" api_key = "CT_TESTY" strings_xml = """<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">MyApp</string> <string name="main_title">Hello World</string> <string name="hello">Hello World</string> </resources>""" payload = { "files": {"strings.xml": strings_xml}, "logcat_lines": 200, "api_key": api_key, } r = requests.post(url, json=payload, timeout=60) data = r.json() print(data) pid = data['project_id'] url = f"https://droidloop.dev/v1/project/{pid}/poll" while True: st = requests.get(url, params={"api_key": api_key}, timeout=15).json() if st.get("error"): raise SystemExit(st) if st.get("active") == 1: print("active — expires_in=", st["expires_in"]) break if st.get("queued") == 0 and st.get("active") == 0: print("finished or failed", st) exit(1) time.sleep(2) url = "https://droidloop.dev/v1/project/action" requests.post(url, json={ "project_id": pid, "api_key": api_key, "action": "tap", "coords": [360, 800], }, timeout=15) requests.post(url, json={ "project_id": pid, "api_key": api_key, "action": "screenshot", }, timeout=15) data = requests.post( "https://droidloop.dev/v1/project/status", json={"project_id": pid, "api_key": api_key}, timeout=30, ).json() if data.get("error"): raise SystemExit(data) for i, b64 in enumerate(data.get("screenshots") or []): open(f"CT_{i}.png", "wb").write(base64.b64decode(b64)) print(data.get("project_url")) print(data.get("project_zip_url")) print(data.get("apk_zip_url"))
#!/usr/bin/env bash set -euo pipefail API_KEY="CT_TESTY" BASE="https://droidloop.dev" CREATE=$(curl -sS -X POST "$BASE/v1/project" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg k "$API_KEY" '{ api_key:$k, logcat_lines:200, files:{"strings.xml":"<resources><string name=\"app_name\">MyApp</string></resources>"} }')") echo "$CREATE" PID=$(echo "$CREATE" | jq -r '.project_id') while true; do ST=$(curl -sS -G "$BASE/v1/project/${PID}/poll" \ --data-urlencode "api_key=$API_KEY") echo "$ST" [[ $(echo "$ST" | jq -r '.active // 0') == "1" ]] && break [[ $(echo "$ST" | jq -r '.error // 0') == "1" ]] && exit 1 sleep 2 done curl -sS -X POST "$BASE/v1/project/action" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg p "$PID" --arg k "$API_KEY" \ '{project_id:$p, api_key:$k, action:"tap", coords:[360,800]}')" curl -sS -X POST "$BASE/v1/project/action" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg p "$PID" --arg k "$API_KEY" \ '{project_id:$p, api_key:$k, action:"screenshot"}')" STATUS=$(curl -sS -X POST "$BASE/v1/project/status" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg p "$PID" --arg k "$API_KEY" \ '{project_id:$p, api_key:$k}')") echo "$STATUS" | jq . echo "$STATUS" | jq -r '.screenshots // [] | to_entries[] | "\(.key) \(.value)"' \ | while read -r i b64; do echo "$b64" | base64 -d > "CT_${i}.png" done
0 / 1 — never JSON booleans.200 even when error=1. Always read the JSON body.error_msg is present on every response. Empty string means no error.api_key, or Authorization: Bearer …, or X-API-Key.1¢ upfront on a successful request — even if the later build fails.error_msg on every response so an AI agent can react without a browser in the loop.files, no connected worker, build failure (with a tail of the build log appended to error_msg).Droidloop · Agentic Android API · 1 credit = $0.01