# PreviewAPK PreviewAPK is the build-to-preview loop for Android: change the app, build, and the new build runs on a cloud phone in seconds — no Android Studio, no local SDK, no emulator to run, no Play Store upload just to test a feature. The developer watches the real app live in the browser. An AI agent gets eyes and ears inside the app it builds: it pushes builds, drives the app, sees its screen (screenshots), reads its UI (hierarchy), and hears its logs — so it verifies its own work instead of building blind, and the human sees the feature the moment it's proven. Every device also carries a view-only link for showing the session to anyone — a byproduct of the loop, never the driver's seat. Site: https://previewapk.siteport.site Guide for humans and agents: https://previewapk.siteport.site/agents ## The model - An account owns device slots ($19/month per slot). One slot = one device. - Every session starts from a fresh phone. Pushing a different app replaces the previous one — nothing carries over between sessions. - Driving happens only with credentials: the owner in the portal, or an API key (CLI / CI / AI agent). Links NEVER drive — links only watch. ## Links - Session link: https://previewapk.siteport.site/v/ The only link there is. Created per session; dies the moment the session ends (stop or idle timeout). View-only — its page carries a QR of the link itself so a viewer can move to their phone. Get it from the links action once the session is live, and give it to the human. There is no permanent link: a link that never expires is a leak waiting for a timestamp. ## Driver modes A session is driven by exactly one side, chosen at start: - driver=agent — the API-key caller (you, the agent) may tap/type/screenshot; the owner watches in the portal. - driver=user — the owner drives in the portal; API input is refused (403). POST /portal/api/device//handoff {"driver":"user"|"agent"} switches sides on a live session without rebooting. The owner can always stop. Sessions auto-stop after 15 minutes idle (any input resets the timer). ## API (base https://previewapk.siteport.site, header: X-Api-Key: ) - GET /portal/api/state — devices, attached apps, links, session state - POST /portal/api/upload — multipart form: field "apk" (the file), field "device" (REQUIRED — device id or name; there is no default device: calls that do not name a device fail). Replaces the device's app. Each upload is identified as it lands: the package, versionCode, and versionName are read from the APK's manifest. Uploading bytes the account already has stored reuses the stored copy instead of storing it twice ("deduped": true). Installs into a session are verified against the uploaded versionCode (install_version_mismatch on a mismatch), and installing an OLDER versionCode over a live session's app is refused (409 version_downgrade) unless the request carries replace=true (field on upload/attach, {"replace": true} on attach-recent, --replace on the CLI, replace on the MCP push) — replace uninstalls the installed app first and says so; its data is deleted. - POST /portal/api/devices {"name","profile","android"} — create a device in a free slot (403 when none is free) - POST /portal/api/device//run {"driver":"agent"|"user"} — start session - POST /portal/api/device//stop - POST /portal/api/device//input {"action":"tap","x":0.5,"y":0.4} actions: tap {x,y}, swipe {x1,y1,x2,y2,dur}, key {key} (Android keycode: 4=Back, 3=Home, 187=Recents, 66=Enter), text {text} (types into the focused field), launch {} (bring the app back to the foreground), observe {} (a compact numbered snapshot: snapshot_id + elements with refs), tap_ref {snapshot_id, ref} (tap a snapshot element; the server re-checks it first — 409 stale_snapshot if the screen moved on), wait_for {text|desc|id, present, timeout_s} (poll until the element is present or gone; answers {found, waited_ms}), act {actions[], observe} (a batch of up to 20 actions in order; stops at the first failure; never auto-replayed), record_start {name?} / record_stop {} / record_status {} (screen recording to MP4 — one recording caps at 180s; stop pulls the MP4 to the server and answers {rec_id, size, duration_s}; a recording in progress is saved automatically when the session ends or the phone reboots), device {op, ...} (device controls: timezone {tz} (IANA, verified on the device), locale {locale, package?} (per-app language), location {lat, lon} (mock GPS fix) / location_clear, file_push {path, data_b64} / file_list {path} / file_delete {path} (shared storage only, 10 MB cap), reboot {} (restart the phone — same phone, same data; app relaunched). Coordinates are 0..1 screen fractions. No clipboard control: not available on these phones. Errors use the envelope: {error, code, retryable, next, request_id} (request_id also arrives as the X-Request-Id header); ambiguous selectors return their candidates instead of guessing. - GET /portal/api/device//screenshot — current screen as a PNG. Supports Range requests — a cut download resumes where it stopped — and ?scale=0.5 for a quarter-size transfer on weak networks. - GET /portal/api/device//hierarchy — current screen as a pruned UI element tree (class, text, desc, id, bounds, fractional center, state flags). Navigate and assert with the tree; verify visual correctness with screenshots. On Flutter and other semantics-driven UIs, labels live in desc, not text. - GET /portal/api/device//recording/ — a finished recording's MP4. The recording's session must be this device's current or an archived session, so recordings stay downloadable after the session ends. Range requests resume a cut transfer. - GET /portal/api/device//file?path=Download/x — pull a file (max 10 MB) from the device's shared storage. Driver-gated; Range-resumable. - POST /portal/api/device//handoff {"driver":"user"|"agent", "note"?} — handing the wheel back to the agent can carry the human's note, delivered to the agent exactly once (next state read or input result, as handback_note + resumed); the handback also clears any pending request-human - POST /portal/api/device//request-human {"reason"} — the driving agent asks the human for a step only a human should do; answers {handoff, portal_url, message_for_human} (relay verbatim). Device state carries handoff {pending, reason, requested_at} while waiting - POST /portal/api/device//finish {"summary", "verdict"?, "end_session"?} — the wrap-up: the response composes message_for_human from the server's own counts (duration, actions, errors — counted by the server as the calls happen: a driving call's error counts, and act batches count their failed actions) and stores finish_report on the session + its archive. end_session defaults to false - POST /portal/api/feedback {worked, broke, wish, workaround, rating} — a field report from the agent that drove the session - POST /portal/api/device//links — {session_url, status, driver, driver_changed_at, driver_note, handoff, finish_report} - GET /portal/api/device//logs/sessions — the device's archived session logs; GET /portal/api/device//logs/session/ — one session's log lines (the live one included) - POST /portal/api/keys {"name", "scope", "expires_days"} / POST /portal/api/keys/revoke {"id"} — API-key management: portal session, or an admin-scope key (agent/readonly keys cannot manage keys). Scopes: agent (default — full behavior), readonly (observe only: state, hierarchy/observe, screenshots, logs, links, recording status/download, file list/pull; every driving/mutating call gets 403 key_readonly), admin (agent + the keys API). expires_days: 30|90|365 or omitted for never; an expired key gets 401 key_expired. Every API-key call is written to the account's activity log (time, key, action, device, result, request_id — never bodies or typed text), kept 30 days; human state responses include the latest 50 as "activity". Session-log archive entries carry duration_seconds, action_count (individual + batched actions), error_count, the agent's finish_report when one was filed, and end_reason. - POST /portal/api/privacy {"share_diagnostics": bool} — the account's diagnostic-sharing preference, portal session only - POST /portal/api/device//install-link {"src"} — mint a 15-minute "Install on my phone" link, portal session only - POST /portal/api/setup/start {"label"?} -> {code, url, expires_in} and POST /portal/api/setup/token {"code"} — the agent setup grant behind `previewapk setup` (see Setup below) Poll GET /portal/api/state after starting: status goes starting -> live (typically 30–60s). Input before live returns 409. ## Setup (agent-led) previewapk setup — the human never pastes a key into a chat. Setup prints a short code and the portal grant URL; the human approves the code while signed in; the portal mints a normal API key and setup saves it to ~/.previewapk/credentials.json (0600), which both clients read when PREVIEWAPK_API_KEY is unset (the env var wins). Full instructions for agents: https://previewapk.siteport.site/setup.md The agent skill: https://previewapk.siteport.site/skills/previewapk/SKILL.md ## CLI curl -o previewapk https://previewapk.siteport.site/cli/previewapk.py && chmod +x previewapk export PREVIEWAPK_API_KEY= # or run: previewapk setup ./previewapk push app.apk --run # upload + start (agent-driven) ./previewapk look # compact numbered snapshot of the screen (refs you can tap) ./previewapk shot screen.png # see the current screen (PNG) ./previewapk tap 0.5 0.6 # drive (or tap --text "...", or tap --ref N --snapshot ) ./previewapk wait --text "Welcome" # wait until an element appears (exit 3 on timeout) ./previewapk act batch.json # run a batch of up to 20 actions in order ./previewapk record start # record the screen (MP4, max 180s); record stop / status / pull ./previewapk device timezone America/New_York # device controls: timezone, locale , ./previewapk device location 40.71 -74.0 # mock GPS, push/pull/ls/rm files, reboot ./previewapk handoff user # hand the wheel to the human ./previewapk request-human "the app is asking for a 2FA code" # ask the human for a step only they should do ./previewapk wait --for driver # wait until the human hands the wheel back (prints their note) ./previewapk finish --summary "checked the login flow" --verdict passed # file the wrap-up; relay its message verbatim ./previewapk links # the one share link for the live session ./previewapk tree # current screen as a UI element tree ./previewapk relaunch # bring the app back to the foreground ./previewapk feedback --broke "..." --wish "..." # file a field report Every command supports --json. Reliability: the CLI and MCP verify instead of guessing. Screenshot downloads resume where a cut connection left off; if an input call's response dies in transit, the tool checks the device and reports landed / not landed / unknown (CLI exit code 2 = unknown) instead of a bare failure. Navigate with the tree; use screenshots for visual proof. ## MCP Hosted (no install): POST https://previewapk.siteport.site/mcp with header "Authorization: Bearer " — MCP Streamable HTTP, the same 27 tools. The server issues an Mcp-Session-Id at initialize and your working-device selection rides on it (two connections never share a selection; selections are kept for about two idle hours and are forgotten when the service restarts; calls that name their device also work statelessly). Claude Code: claude mcp add --transport http previewapk https://previewapk.siteport.site/mcp --header "Authorization: Bearer ". Hosted differences (the server cannot read your disk): push_apk takes apk_b64 (base64) instead of path; device push_file takes data_b64, pull_file returns data_b64; record_download returns the MP4 as data_b64. Bodies cap at 25 MB. Local (stdio fallback): curl -o previewapk_mcp.py https://previewapk.siteport.site/mcp/previewapk_mcp.py 27 tools: previewapk_status, previewapk_push_apk, previewapk_start, previewapk_stop, previewapk_get_links, previewapk_tap, previewapk_swipe, previewapk_press_key, previewapk_type_text, previewapk_screenshot, previewapk_hierarchy, previewapk_tap_text, previewapk_observe (a compact numbered snapshot of the screen — the cheap default read), previewapk_act (a batch of up to 20 actions in order; stops at the first failure; never replay a batch whose outcome you don't know), previewapk_wait_for (wait until an element is present or gone — or, with condition "driver", until the human hands the wheel back), previewapk_request_human (ask the human for a step only a human should do — password, 2FA, payment, CAPTCHA — and get back the exact message to relay; the human does the step on the phone and hands the wheel back, and the handback reaches the agent by itself), previewapk_finish (file the session wrap-up: the server composes the counts — duration, actions, errors — and the message to relay to the human; finishing does not stop the session unless end_session is set), previewapk_record_start / previewapk_record_stop / previewapk_record_download (record the screen to an MP4 and download it — recordings stay downloadable after the session ends), previewapk_device (device controls: set_timezone, set_locale (per-app language), set_location / clear_location (mock GPS), push_file / pull_file / list_files / delete_file in shared storage, reboot (same phone, same data)), previewapk_handoff, previewapk_relaunch, previewapk_feedback (file a field report: what broke, what worked, what you wish it had, what you had to work around — these are read and tallied, and they set the roadmap), previewapk_get_logs (the app's logcat for the session — errors and crash traces; also available after the session ends as a saved tail), previewapk_list_devices and previewapk_use_device. Agents work like a shell: list devices, SELECT a working device (use_device, or start — which requires naming the device and selects it), then every other tool talks to the working device without repeating its name. A per-call 'device' argument (NAME or id) overrides for one call. With nothing selected, action tools refuse — no default device is ever guessed, and each agent session holds its own selection. The screenshot tool returns an image the agent can see; tap_text taps an element by its label. ## Typical agent loop 1. Build the APK. 2. previewapk_push_apk (or CLI push) with the new build. 3. previewapk_start with driver="agent". 4. Poll status until live; screenshot; tap/type through the flow; screenshot again to verify the change actually looks right. 5. previewapk_handoff to "user" and give the human the session link (from previewapk_get_links): "I built it and tested it — here's the live app, take a look." 6. previewapk_finish with a summary of what was tested — relay its message_for_human verbatim (its counts come from the server). If a step mid-flow needs the human (a password, a 2FA code), previewapk_request_human asks for it and previewapk_wait_for with condition "driver" waits for the handback. ## Documentation - https://previewapk.siteport.site/docs/ — getting started & concepts - https://previewapk.siteport.site/docs/api/ — full API reference - https://previewapk.siteport.site/docs/cli/ — CLI reference - https://previewapk.siteport.site/docs/mcp/ — MCP server & tools - https://previewapk.siteport.site/docs/ci/ — CI recipes - https://previewapk.siteport.site/docs/prompts/ — prompt library (smoke check, exploratory, bug repro, accessibility, release checklist, a flow with a human step + wrap-up) - https://previewapk.siteport.site/setup.md — agent-led setup instructions - https://previewapk.siteport.site/skills/previewapk/SKILL.md — the agent skill - https://previewapk.siteport.site/llms-full.txt — the full documentation in one file - https://previewapk.siteport.site/openapi.yaml — OpenAPI 3.1 spec of the API