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

# First session

> Acquire a device, observe it, act on it, release it, one call at a time.

A session is always the same four beats: acquire a lease, observe, act under
that lease, release. This page walks one session end to end and shows what
each call returns.

Every example is a tool call. Your client decides how you phrase it; the
parameters and the results below are what travels on the wire.

<Steps>
  <Step title="See what is reachable">
    `list_devices` needs no lease and takes no parameters. Call it first,
    every time.

    ```json theme={null}
    { "name": "list_devices", "arguments": {} }
    ```

    Each entry carries an id, a status, and a `capabilities` block:

    ```json theme={null}
    {
      "devices": [
        {
          "deviceId": "black-arm-01",
          "backendKind": "robot_hand",
          "status": "online",
          "capabilities": {
            "fidelityTier": "physical",
            "actions": { "tap": true, "swipe": true, "typeText": "per_key_visual", "openApp": "visual", "pressKey": ["home"] },
            "observations": { "screenshot": "camera", "uiTree": false, "framesAreReferenceable": true, "freshnessMs": null },
            "os": "ios",
            "coordSpace": { "width": 1000, "height": 1000 },
            "physical": { "hover": false, "pressDepth": false, "queueControl": false }
          },
          "metadata": {}
        }
      ]
    }
    ```

    <Note>
      Every payload on this page is illustrative. Ids, timestamps, screen
      geometry and freshness windows come from your own devices, so read them
      out of the response rather than copying them from here.
    </Note>

    Read `capabilities` before you plan anything. A device with
    `uiTree: false` has no element tree to query, and a device whose
    `pressKey` list omits a key refuses that key. See
    [Capabilities](/devices/capabilities).

    If you already know the id, `get_device` answers the same shape for one
    device without enumerating the fleet.
  </Step>

  <Step title="Take a lease">
    A device holds one lease at a time, across every org. While you hold one,
    no other agent can acquire the device, and every action and observation
    you send must carry the `leaseId` it returned. A signed-in person in your
    org may still act on the device from the console beside you; when they do,
    your results say so in `others`.

    ```json theme={null}
    { "name": "acquire_device", "arguments": { "deviceId": "black-arm-01" } }
    ```

    ```json theme={null}
    {
      "orgId": "org_local",
      "deviceId": "black-arm-01",
      "leaseId": "lease_9f2c...",
      "fencingToken": 41,
      "acquiredAt": "2026-08-29T10:14:02.113Z",
      "expiresAt": "2026-08-29T10:24:02.113Z"
    }
    ```

    Keep `leaseId`. Read `expiresAt` rather than assuming a duration: a lease
    expires on its own, so a client that crashes cannot strand a device. You
    never pass `fencingToken` anywhere; the control plane uses it to make a
    resumed or duplicated caller lose to the current lease holder.

    Acquiring a device that is already leased fails. That is the point of a
    lease, and it is why repeating `acquire_device` is not idempotent.
  </Step>

  <Step title="Observe">
    ```json theme={null}
    { "name": "screenshot", "arguments": { "deviceId": "black-arm-01", "leaseId": "lease_9f2c..." } }
    ```

    The result carries the image itself plus the metadata you need next:

    ```json theme={null}
    {
      "format": "jpeg",
      "source": "camera",
      "pixelSize": { "width": 1179, "height": 2556 },
      "frameId": "frm_5b81...",
      "sequence": 12,
      "capturedAt": "2026-08-29T10:14:06.902Z"
    }
    ```

    `pixelSize` is informational. Do not aim with it. Aim on the normalized
    grid, where both axes run 0 to 1000 whatever the real screen size is.

    Keep `frameId`. It is how the next step proves it was decided on this
    picture of the screen.

    On a device with a UI tree, `ui_snapshot` gives you the same screen as
    structured elements, and every bound inside that tree is already on the
    0 to 1000 grid, so a coordinate you read there can be tapped directly.
  </Step>

  <Step title="Act">
    ```json theme={null}
    {
      "name": "tap",
      "arguments": {
        "deviceId": "black-arm-01",
        "x": 500,
        "y": 820,
        "leaseId": "lease_9f2c...",
        "idempotencyKey": "signin-button-1",
        "observedFrame": "frm_5b81..."
      }
    }
    ```

    Two optional parameters are worth passing on every action.

    `observedFrame` binds the action to the frame you decided on. If the
    screen has moved on since that frame, the tap is refused with
    `stale_frame` rather than landing somewhere you did not intend. Observe
    again and decide again.

    A frame does **not** age out on our side, and a newer screenshot does not
    replace it. Take as long as you need between observing and acting,
    including time spent reading a large `ui_snapshot` or waiting for a person
    to approve the step, and observe again in between if that helps.

    What the frame is for is the `others` report that comes back on every
    action: how many actions another principal took on this device since you
    looked, when the newest was, and whether the hands were a person's or an
    agent's. With a frame named, an agent's action is refused rather than
    taken on a screen somebody else has touched; pass
    `refuseIfOthersActed: false` to act anyway and decide from `others`.

    A device whose own resource expires frames can still answer `expired`; a
    non-`null` `freshnessMs` in its capabilities is how you know to expect it.
    Nothing is lost when a frame is refused for any reason: the action never
    began and your `idempotencyKey` was not consumed, so re-observe and act
    under the same key.

    `idempotencyKey` is your own request id. Reuse the same value when
    retrying the same intended action, and the recorded outcome is replayed
    instead of the gesture happening twice.

    The result tells you the device applied it:

    ```json theme={null}
    {
      "status": "committed",
      "committedAt": "2026-08-29T10:14:08.377Z",
      "idempotencyKey": "signin-button-1"
    }
    ```

    `committed` means the device confirmed the action, not that the request
    was accepted. On the physical tier the actuator has finished moving by the
    time you read this.

    Because you passed an `idempotencyKey`, this outcome is also on the
    ledger: `get_receipt` with the same key returns it later, which is how you
    confirm an action whose response you never received. See
    [Reading a trace](/getting-started/trace).

    The other action tools take the same shape: `swipe`, `long_press`,
    `type_text`, `press_key`, and `open_app`. Every one of them is destructive
    by design, because a gesture on a real device cannot be undone.
  </Step>

  <Step title="Release">
    ```json theme={null}
    { "name": "release_device", "arguments": { "deviceId": "black-arm-01", "leaseId": "lease_9f2c..." } }
    ```

    ```json theme={null}
    { "ok": true, "deviceId": "black-arm-01" }
    ```

    `leaseId` is required here, unlike on the action tools where it is
    optional: giving a device back is about one particular lease, so the id
    has to be the one you were given.

    Releasing a device you no longer hold is a no op, so a release in a
    `finally` block is safe.
  </Step>
</Steps>

## What happens if you skip a step

| Mistake | What you get |
| - | - |
| Acting without `leaseId` | Nothing to fix: the action runs under the lease your organization holds, or takes the device for you |
| Acting on a device another organization holds | `device_unavailable`, the same answer as a device you cannot see |
| Releasing with an id that is not the current lease | `lease_mismatch` |
| Acting on a stale `observedFrame` | `stale_frame`, telling you to observe again |
| Asking for an ability the device lacks | `capability_unsupported`, never a silent no op |
| Misspelling a parameter | The call is refused. Unknown keys are rejected rather than dropped |

Read [Reading a trace](/getting-started/trace) next for how to confirm an
action whose response you never received.


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