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

Run a task

/v1/leases/{lease}/taskPOST
Do a task in plain words ("open the TikTok likes tab") on the lease's iPhone. An agent on ghosthand.dev (a fast model, Claude Haiku 5.5) reads the screen with OCR and sends the taps, swipes and keys through this Mac's tunnel, then answers in one or two sentences. One call is one turn; the task keeps its history, so task sends a follow-up to the same task. A follow-up while a turn runs stops that turn first. The task ends one day after its last turn. The agent gets the iPhone's name, model, iOS version, language and region (as info reads them).
With Accept: text/event-stream the response is the turn's AI SDK UI message stream, unchanged: data: <chunk JSON> events (start, tool-input-available, tool-output-available, text-delta, finish, ...), then data: [DONE]. A failure is an error chunk. Closing the stream stops the task. Without that header the response is one JSON body after the turn ends.
Needs a signed-in Mac with its tunnel connected. The task uses the lease; do not send other input to the iPhone while it runs.
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
prompt?string
What to do, in plain words. Without it, task must be running and the call follows its turn.
Example: "open the TikTok likes tab"Min length: 1
task?string
Id of an earlier task (Ghosthand-Task), to continue it with its history
Pattern: ^[A-Za-z0-9_-]{8,64}$
Response
200 · The turn ended (JSON) or streams (SSE)
task *string
The task id; pass it as task to send a follow-up
text *string
The agent's last answer
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/42/task" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "prompt": "open the TikTok likes tab", "task": "string" }'
Response example
{ "code": "device-leased", "title": "iPhone leased by another client", "status": 409, "detail": "Tommy's iPhone is in use by another lease:\n ses_456 (book a table on OpenTable), acquired 2 min ago, last used 10 s ago (tap)\nDecide first: if that task is still active, give up or tell the user the iPhone is busy." }