Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Take exclusive use of one iPhone

/v1/leasesPOST
Returns a new lease id for the routes under /v1/leases/{lease}/. Fails at once with device-leased while another lease holds the iPhone. The problem detail shows its label, reason, when it was acquired and when it was last used, so the client can decide to give up instead of retrying. force ends that lease instead (after its running operation); its next call gets lease-invalid. waitSeconds waits for the other lease to end instead. Send Accept: text/event-stream with it, so a closed connection stops the wait (events log, then result with the Lease, or error). On the unix socket, a device that is on another Mac of the account (or the only iPhone of the account, when this Mac has none) is taken on that Mac; the id is then <mac>:<number>. Every acquire asks ghosthand.dev if the plan covers this iPhone on this Mac (free: one iPhone; Pro: every iPhone on each paid Mac). Not covered: subscription-required with the subscribe and dashboard links. ghosthand.dev unreachable: only an iPhone allowed in a recent check is leased; any other gets tunnel-offline.
Authorizationoptional
bearerAuth?Bearer <token>
Required on the tunnel. An API token of the account, from ghosthand tunnel url --new-token. The unix socket needs no token
Request Bodyapplication/jsonrequired
reason *string
The task the iPhone is used for, shown to others in GET /v1/devices and device-leased errors, so they can decide to wait or give up
Example: "reply to Anna in Messages"Min length: 1
device?string
USB id or name from GET /v1/devices, or default when exactly one iPhone is connected
Default: "default"Min length: 1
mac?string
Slug of the Mac the iPhone is on (mac from GET /v1/devices). On the unix socket, another Mac's slug sends the acquire straight to that Mac, without listing every Mac first. Through the tunnel it must be this Mac's slug or omitted.
Example: "studio"Min length: 1
label?string
Optional display text shown to others in GET /v1/devices and device-leased errors, e.g. an agent session id. Not checked and not unique
Example: "ses_123"
idleTimeoutSeconds?number
The lease ends this long after its last call
Default: 600Min: 1Max: 86400
force?boolean
End another lease on the iPhone and take it. Only for a stale lease whose holder is gone.
Default: false
waitSeconds?number
While another lease holds the iPhone, retry every second for up to this long instead of failing with device-leased. While it waits, GET /v1/phones lists the client under waiting. Cannot be combined with force.
Min: 1Max: 3600
Response
201 · Lease taken. JSON by default, server-sent events with `Accept text/event-stream`.
id *string
Lease id: the slug of the Mac that holds it and a decimal number that only increases, e.g. studio:42 (42 when that Mac is signed out). Pass it back exactly as returned. Whoever has it can use the iPhone.
Example: "42"Pattern: ^([a-z0-9-]+:)?[1-9][0-9]*$
device *string
USB capture device id
deviceName *string
label?string
reason *string
idleTimeoutSeconds *number
expiresAt *string (date-time)
When the lease ends if no call uses it before. Each call moves it.
replaced?object
The lease that force ended, if any
Show properties
label?string
Display text the holder gave, e.g. an agent session id
reason *string
The task the iPhone is used for
acquiredAt *string (date-time)
lastUsedAt *string (date-time)
The last call on the lease (or acquiredAt before the first)
lastCommand?string
CLI name of the last call on the lease, e.g. tap or screenshot. Absent before the first
expiresAt *string (date-time)
When the lease ends if no call uses it before
notice?string
A note from ghosthand.dev about the plan, e.g. a failed payment. Show it to the user.
Default · Error (RFC 9457 problem details). `code` is stable; switch on it, not on `title` or `detail`.
code *"invalid-request" | "unauthorized" | "not-found" | "device-not-found" | "no-device" | "multiple-devices" | "permission-denied" | "bluetooth-unavailable" | "bluetooth-disconnected" | "no-frame" | "device-locked" | "lock-failed" | "pairing-timeout" | "pointer-not-found" | "lock-state-unavailable" | "lockdown-unavailable" | "unlock-failed" | "mute-unconfirmed" | "text-not-found" | "wait-text-timeout" | "cancelled" | "lease-invalid" | "device-leased" | "recording-active" | "no-recording" | "recording-failed" | "daemon-busy" | "tunnel-offline" | "login-required" | "subscription-required" | "unsupported-in-mode" | "xctest-unavailable" | "task-failed" | "internal-error"
Stable error code. Switch on it; title and detail are for people.
title *string
Short summary of the code, the same for every error with this code
status *integer
HTTP status of the error. In stream mode the response status stays 200.
detail *string
What happened and what to do next. Agents should read it.
Request example
curl -X POST "https://ghosthand.dev/macs/studio/v1/leases" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "reason": "reply to Anna in Messages", "label": "ses_123" }'
Response example
{ "id": "42", "device": "7EF219AD-69B4-423E-B8C4-4F4A6140822F", "deviceName": "Tommy's iPhone", "label": "ses_123", "reason": "reply to Anna in Messages", "idleTimeoutSeconds": 600, "expiresAt": "2026-10-07T09:10:00.000Z" }