droidloop
pool 0
in use 0
queued 0
studio explorer api contact Sign in Create account

Agentic Android API

GitHub

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.

01 · create 02 · poll 03 · action 04 · status 05 · full example conventions

01Create project

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.

POST /v1/project

Request

create.py python
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)
create.sh bash
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>"
    }
  }'

Success · 200

response.json json
{
  "project_id": "a1b2c3d4e5",
  "active": 0,
  "queued": 1,
  "expires_in": 0,
  "error": 0,
  "error_msg": "",
  "status": "queued"
}

Error · no worker

response.json json
{
  "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"
}

02Poll queue / lease

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.

GET /v1/project/{project_id}/poll

Wait until active

poll.py python
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)
poll.sh bash
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

03Emulator actions

Only accepted while active=1. Coordinates are device pixels, origin top-left. Every call refreshes the lease.

POST /v1/project/action

Actions

action.py python
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)
action.sh bash
# 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]}'
Note: screenshots taken through /action are appended to the project's screenshot list and also returned by /v1/project/status.

04Fetch results

Returns loot collected after a successful build: base64 PNG screenshots, logcat text, and the studio share URL. Safe to call while still queued.

POST /v1/project/status

Fetch

status.py python
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.sh bash
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

05Full example

A complete runnable script: create a project, poll until active, tap the screen, take a screenshot, fetch status, save all PNGs.

test_api.py python
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"))
test_api.sh bash
#!/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

Conventions

Droidloop · Agentic Android API · 1 credit = $0.01