AkaPhoneDocs
Guides

Install an APK

Install your own app on a device by dragging it into the dashboard, or over the API with a presigned upload, a completion check and an install request.

You can install any APK up to 300 MB on a running device. Files go straight from you to storage through a presigned URL, never through the API server, and the device downloads them from there.

From Claude Code, Cursor or Codex#

The MCP tool device_install_apk does the whole flow from a local file path and waits up to 5 minutes for the result:

Install ./app/build/outputs/apk/debug/app-debug.apk on checkout-test, then open the app and take a screenshot.

In the dashboard#

Open a running device and drag the .apk file onto the page. You see upload progress, then Verifying upload, then the install. When the device reports back you get a toast with the package name, for example Installed com.example.shop, or the installer's error.

Over the API#

The flow has five calls:

  1. POST /v1/uploads declares the file and returns a presigned put_url.
  2. PUT the file bytes to put_url.
  3. POST /v1/uploads/:id/complete confirms the file arrived.
  4. POST /v1/devices/:id/apps installs it on a running device.
  5. GET /v1/devices/:id/apps/:install_id tells you when it is installed.

An upload can be installed on as many devices as you like for 7 days, after which it is deleted.

1. Create the upload#

Shell
FILE=app-release.apk
SIZE=$(wc -c < "$FILE" | tr -d ' ')

curl -s -X POST https://akaphone.abhgnx-417.workers.dev/v1/uploads \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\": \"$FILE\", \"size_bytes\": $SIZE, \"content_type\": \"application/vnd.android.package-archive\"}"
JSON
{
  "upload": { "id": "upl_01m5vqsywgw57haqkjnexz", "filename": "app-release.apk", "size_bytes": 48211337, "status": "pending" },
  "put_url": "https://<account>.r2.cloudflarestorage.com/akaphone-artifacts/apks/usr_01m5vqsywg3mxj7dyxv9q9/upl_01m5vqsywgw57haqkjnexz/app-release.apk?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
  "headers": { "content-type": "application/vnd.android.package-archive" },
  "expires_in": 900
}

Rules for the request:

  • filename: 5 to 128 characters, letters, digits, ., _, - and spaces, starting with a letter or digit and ending in .apk. Spaces become _. .aab bundles are rejected with 400.
  • size_bytes: the exact size of the file in bytes, at most 314,572,800 (300 MB).
  • content_type: optional, defaults to application/vnd.android.package-archive.

2. Upload the bytes#

PUT the file to put_url within expires_in seconds (15 minutes). Send every header from headers, and send exactly size_bytes bytes: the URL is signed for that content type and that length, and storage rejects anything else.

Shell
curl -s -X PUT "$PUT_URL" \
  -H "Content-Type: application/vnd.android.package-archive" \
  --data-binary @"$FILE"

Do not send your Authorization header to put_url; the signature in the URL is the authorization.

3. Complete#

Shell
curl -s -X POST https://akaphone.abhgnx-417.workers.dev/v1/uploads/$UPLOAD_ID/complete \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
JSON
{ "upload": { "id": "upl_01m5vqsywgw57haqkjnexz", "status": "uploaded" } }

AkaPhone checks that the object exists in storage. If you call this before the PUT has finished you get 409 conflict (Object not found in storage). Calling it again after success is harmless.

4. Install#

Shell
curl -s -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices/$DEVICE_ID/apps \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"upload_id\": \"$UPLOAD_ID\"}"
JSON
{ "install_id": "ins_01m5vqsywg99189k5xj7f5" }

The response is 202: the device's host downloads the file and runs adb install -r -g, which replaces an existing install of the same package and grants all runtime permissions up front.

5. Wait for the result#

Poll GET /v1/devices/:id/apps/:install_id until status is installed or failed:

Shell
while :; do
  INSTALL=$(curl -s https://akaphone.abhgnx-417.workers.dev/v1/devices/$DEVICE_ID/apps/$INSTALL_ID -H "Authorization: Bearer $AKAPHONE_API_KEY")
  case $(echo "$INSTALL" | jq -r .status) in installed|failed) break;; esac
  sleep 3
done
echo "$INSTALL"
JSON
{ "install_id": "ins_01m5vqsywg99189k5xj7f5", "status": "installed", "package": "com.example.shop", "created_at": "2026-10-26T09:20:00.000Z", "finished_at": "2026-10-26T09:20:06.000Z" }

On failure error holds the installer's message. An install still pending after 15 minutes fails with timeout. The same result is also pushed as an app.installed event to anyone watching the live stream.

Everything in one script#

#!/usr/bin/env bash
set -euo pipefail
API=https://akaphone.abhgnx-417.workers.dev
AUTH="Authorization: Bearer $AKAPHONE_API_KEY"
FILE=${1:-app-release.apk}
DEVICE_ID=$2
SIZE=$(wc -c < "$FILE" | tr -d ' ')

UP=$(curl -sf -X POST "$API/v1/uploads" -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"filename\": \"$(basename "$FILE")\", \"size_bytes\": $SIZE}")
UPLOAD_ID=$(echo "$UP" | jq -r .upload.id)
PUT_URL=$(echo "$UP" | jq -r .put_url)
CTYPE=$(echo "$UP" | jq -r '.headers["content-type"]')

curl -sf -X PUT "$PUT_URL" -H "Content-Type: $CTYPE" --data-binary @"$FILE"
curl -sf -X POST "$API/v1/uploads/$UPLOAD_ID/complete" -H "$AUTH" > /dev/null
INSTALL_ID=$(curl -sf -X POST "$API/v1/devices/$DEVICE_ID/apps" -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"upload_id\": \"$UPLOAD_ID\"}" | jq -r .install_id)

for i in $(seq 1 100); do
  STATUS=$(curl -sf "$API/v1/devices/$DEVICE_ID/apps/$INSTALL_ID" -H "$AUTH" | jq -r .status)
  [ "$STATUS" != pending ] && break
  sleep 3
done
echo "install: $STATUS"
[ "$STATUS" = installed ]

Notes and limits#

  • APKs only. Devices install with adb install, which cannot install an Android App Bundle, so uploads of .aab files are rejected. Build a universal APK from your bundle (for example with bundletool build-apks --mode=universal) and upload that.
  • No Google Play Services. Devices are AOSP builds. Apps that require Play Services or Play Integrity may install but fail at runtime.
  • ABI. Devices run on x86_64 hosts. Apps that ship only ARM native libraries may fail to install with INSTALL_FAILED_NO_MATCHING_ABIS; include an x86_64 build or a universal APK.
  • Errors. 404 not_found for an unknown upload, 409 conflict if the upload is not complete or the device is not running, 400 invalid_request for a bad file name or size.

Endpoint reference: Uploads API and Devices API › Install an app.