> ## Documentation Index
> Fetch the complete documentation index at: https://phone-harness.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Start your first phone

> Create a session, connect with ADB, drive it with phone-harness.

You need an account (sign-up is open; new accounts start with \$5 of credit), an API key from [Dashboard → API keys](https://phone-harness.com/dashboard/api-keys), and sufficient trial credits. These examples use `curl` and `jq`.

<Steps>
  <Step title="Set your API key">
    Keep your key out of source control. Set it as an environment variable in your terminal or secret manager.

    ```bash theme={null}
    export PHONE_HARNESS_API_KEY="pck_REPLACE_WITH_YOUR_KEY"
    export PHONE_HARNESS_API_URL="https://api.phone-harness.com"
    ```

    Confirm the account:

    ```bash theme={null}
    curl --fail-with-body -sS "$PHONE_HARNESS_API_URL/me" \
      -H "Authorization: Bearer $PHONE_HARNESS_API_KEY"
    ```
  </Step>

  <Step title="Create a 15-minute session">
    Choose a new idempotency key for this logical session. Keep that same key and request body if you need to retry an ambiguous creation response.

    ```bash theme={null}
    REQUEST_KEY="$(python3 -c 'import uuid; print(uuid.uuid4())')"
    curl --fail-with-body -sS "$PHONE_HARNESS_API_URL/sessions" \
      -H "Authorization: Bearer $PHONE_HARNESS_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $REQUEST_KEY" \
      -d '{"timeout_seconds":900}' \
      -o session.json

    SESSION_ID="$(jq -er '.id' session.json)"
    ```

    Successful admission returns HTTP `202` with an `id`, a `state`, and an absolute `expires_at` timestamp. Keep the ID to operate on this session.

    If the request times out or the connection drops, [recover the create receipt](/docs/guides/session-lifecycle#recover-an-ambiguous-create) before starting another phone.
  </Step>

  <Step title="Wait for ready">
    Poll the session until `state` is `ready`. Treat `error`, `closing`, or a `404` as a reason to stop waiting and inspect the result. A two-second polling interval is a client example, not a promised startup time.

    ```bash theme={null}
    curl --fail-with-body -sS \
      "$PHONE_HARNESS_API_URL/sessions/$SESSION_ID" \
      -H "Authorization: Bearer $PHONE_HARNESS_API_KEY" \
      | jq '{id, state, expires_at, ops}'
    ```

    `ready` means the API can use the phone. The browser viewer connects separately.
  </Step>

  <Step title="Connect with ADB">
    The ready session carries its ADB address and unlock code:

    ```bash theme={null}
    curl --fail-with-body -sS \
      "$PHONE_HARNESS_API_URL/sessions/$SESSION_ID" \
      -H "Authorization: Bearer $PHONE_HARNESS_API_KEY" | jq .adb
    ```

    ```json Response theme={null}
    {"host":"live.phone-harness.com","port":22220,"code":"ph_mfwcc5nknqaod034nwojzw0lzq","expires_at":1800000900}
    ```

    Connect stock `adb` and unlock once. This is the only step that needs to run where the phone is used; it needs no API key.

    ```bash theme={null}
    adb connect live.phone-harness.com:22220
    adb -s live.phone-harness.com:22220 shell unlock ph_mfwcc5nknqaod034nwojzw0lzq
    export ANDROID_SERIAL=live.phone-harness.com:22220
    ```
  </Step>

  <Step title="Drive it with phone-harness">
    [phone-harness](https://github.com/ShawnPana/phone-harness) (`pip install phone-harness`) gives you and your agents screenshots, taps, typing and text reading over that connection:

    ```bash theme={null}
    phone-harness <<'PY'
    open_app("Settings")
    tap_text("Network & internet")
    print([o["text"] for o in ocr()][:10])
    PY
    ```

    Plain `adb` works too: `adb install app.apk`, `adb shell input tap 540 1200`, `adb exec-out screencap -p > screen.png`.
  </Step>

  <Step title="End the session">
    ```bash theme={null}
    curl --fail-with-body -sS -X DELETE \
      "$PHONE_HARNESS_API_URL/sessions/$SESSION_ID" \
      -H "Authorization: Bearer $PHONE_HARNESS_API_KEY"
    ```

    HTTP `200` with `released: true` confirms cleanup. HTTP `202` with `cleanup_pending: true` means closure is accepted and cleanup is still running. Your capacity is released after cleanup completes.
  </Step>
</Steps>

<Warning>
  The 900-second duration includes provisioning. There is no separate full 15-minute usage period after readiness. Ending a session deletes its temporary phone and app data.
</Warning>

<CardGroup cols={2}>
  <Card title="Connect a coding agent" icon="robot" href="/docs/use-cases/connect-coding-agent">Give Claude Code, Codex or your own agent fleet a phone each.</Card>
  <Card title="Connect with ADB" icon="terminal" href="/docs/guides/connect-adb">Sharing, reset, off/on, Appium, reconnects.</Card>
</CardGroup>

## The same flow in Python

The steps above, as one program using Python's standard library. It gives creation a stable idempotency key, waits for readiness within its own budget, connects ADB for a screenshot, and requests cleanup in `finally`, recovering the create receipt if the POST's response was lost.

<Note>
  This is a real API example. Running it with a valid account key creates a metered session. It contains no credentials.
</Note>

```bash theme={null}
export PHONE_HARNESS_API_KEY="pck_REPLACE_WITH_YOUR_KEY"
python3 phone_example.py
```

```python theme={null}
import subprocess
import json
import os
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path

API = "https://api.phone-harness.com"
KEY = os.environ["PHONE_HARNESS_API_KEY"]


def request(method, path, body=None, request_key=None):
    headers = {"Authorization": f"Bearer {KEY}"}
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        data = json.dumps(body).encode()
    if request_key is not None:
        headers["Idempotency-Key"] = request_key
    req = urllib.request.Request(
        API + path, data=data, headers=headers, method=method
    )
    with urllib.request.urlopen(req, timeout=40) as response:
        return json.load(response)


request_key = str(uuid.uuid4())
print("Create request key:", request_key)  # A receipt key, not a credential.
sid = None
try:
    # Reuse this key and this exact body if the creation result is ambiguous.
    created = request(
        "POST", "/sessions", {"timeout_seconds": 900}, request_key
    )
    sid = created["id"]
    deadline = time.monotonic() + 180  # Example client wait budget.
    while True:
        session = request("GET", f"/sessions/{sid}")
        if session["state"] == "ready":
            break
        if session["state"] in ("error", "closing"):
            raise RuntimeError(session.get("error", session["state"]))
        if time.monotonic() >= deadline:
            raise TimeoutError("Client stopped waiting for readiness")
        time.sleep(2)

    adb = session["adb"]
    serial = f"{adb['host']}:{adb['port']}"
    subprocess.run(["adb", "connect", serial], check=True)
    subprocess.run(["adb", "-s", serial, "shell", "unlock", adb["code"]], check=True)
    png = subprocess.run(["adb", "-s", serial, "exec-out", "screencap", "-p"],
                         check=True, capture_output=True).stdout
    Path("screenshot.png").write_bytes(png)
    print("Saved screenshot.png")
finally:
    if sid is None:
        # A lost POST response can still have admitted a session.
        try:
            receipt = request("GET", f"/sessions/requests/{request_key}")
            if not receipt["cleanup_complete"]:
                sid = receipt["id"]
        except Exception as recovery_error:
            print("Receipt recovery did not complete:", type(recovery_error).__name__)
            print("Keep the create request key and follow the retry guide.")
    if sid is not None:
        try:
            closed = request("DELETE", f"/sessions/{sid}")
            print("Cleanup response:", closed)
        except Exception as cleanup_error:
            print("Closure not confirmed:", type(cleanup_error).__name__)
            print("Recover the receipt with request key:", request_key)
```

If closure returns `cleanup_pending: true`, poll the create receipt until `cleanup_complete: true`. A failed receipt lookup is not proof that no phone was created: keep the request key and [recover it](/docs/guides/session-lifecycle#recover-an-ambiguous-create). In a production integration, store request keys durably and recover unfinished requests when your process restarts.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.