{"openapi":"3.1.0","info":{"title":"Reach","version":"1.0.0","description":"Reach hands tasks to a computer-use agent running on the owner's Macs under her own Claude subscription: create a task for one of them, watch its step log and screenshots, answer permission prompts, and send follow-up messages, stop, or continue commands."},"servers":[{"url":"https://reach.peaceakinwale.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An agent token minted in Reach Settings, sent as `Authorization: Bearer rk_...`."},"cronSecret":{"type":"http","scheme":"bearer","description":"The `CRON_SECRET` environment value, sent as `Authorization: Bearer <secret>`. Used only by the cleanup cron, never by agents or the owner."}},"schemas":{"Task":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"origin":{"type":"string","enum":["web","api","slack","session"]},"requester":{"type":["string","null"]},"title":{"type":["string","null"]},"prompt":{"type":"string"},"status":{"type":"string","enum":["queued","running","needs_you","done","failed","cancelled"]},"result":{"type":["string","null"]},"error":{"type":["string","null"]},"needs_you_reason":{"type":["string","null"]},"started_at":{"type":["string","null"],"format":"date-time"},"finished_at":{"type":["string","null"],"format":"date-time"},"step_count":{"type":"integer"},"host":{"type":"string","description":"The id of the Mac the task runs on (see `listHosts`)."}},"required":["id","created_at","updated_at","origin","requester","title","prompt","status","result","error","needs_you_reason","started_at","finished_at","step_count","host"]},"Event":{"type":"object","properties":{"id":{"type":"integer"},"task_id":{"type":"string","format":"uuid"},"ts":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["step","progress","screenshot","status","permission_request","permission_verdict","user_message","stop","needs_you","result"]},"text":{"type":["string","null"]},"data":{"type":"object"},"screenshot_path":{"type":["string","null"]},"screenshot_url":{"type":"string","description":"Present only on `screenshot` events; a 10-minute signed URL."}},"required":["id","task_id","ts","kind","text","data","screenshot_path"]},"Permission":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"task_id":{"type":"string","format":"uuid"},"request_id":{"type":"string"},"tool_name":{"type":"string"},"description":{"type":"string"},"input_preview":{"type":"string"},"status":{"type":"string","enum":["open","allow","deny","expired"]},"created_at":{"type":"string","format":"date-time"},"answered_at":{"type":["string","null"],"format":"date-time"}},"required":["id","task_id","request_id","tool_name","description","input_preview","status","created_at","answered_at"]},"Host":{"type":"object","properties":{"id":{"type":"string","description":"The Mac's id, e.g. `macbook-pro`. Pass it as `host` to `createTask`."},"label":{"type":["string","null"],"description":"What the web app calls it, e.g. \"MacBook Pro\"."},"online":{"type":"boolean"},"lastHeartbeat":{"type":["string","null"],"format":"date-time"},"currentTaskId":{"type":["string","null"],"format":"uuid"},"currentTaskTitle":{"type":["string","null"]},"currentTaskStatus":{"type":["string","null"]},"currentTaskUpdatedAt":{"type":["string","null"],"format":"date-time"}},"required":["id","label","online","lastHeartbeat","currentTaskId","currentTaskTitle"]},"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}}},"paths":{"/api/hosts":{"get":{"operationId":"listHosts","summary":"Which Macs are there, and what is each one doing?","description":"Every Mac a task can run on, in label order. `id` is what `createTask` takes as `host`. `online` is the host's own flag and is not proof of life on its own — a host killed without a clean shutdown leaves it `true` — so treat a Mac as up only when `lastHeartbeat` is within 90 seconds. A Mac that is not up is usually asleep, not broken: it wakes within about a minute of a task being queued for it, if it is switched on.","responses":{"200":{"description":"Each Mac's status.","content":{"application/json":{"schema":{"type":"object","properties":{"hosts":{"type":"array","items":{"$ref":"#/components/schemas/Host"}}},"required":["hosts"]}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tasks":{"get":{"operationId":"listTasks","summary":"List recent tasks","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50}}],"responses":{"200":{"description":"Recent tasks, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"tasks":{"type":"array","items":{"$ref":"#/components/schemas/Task"}}},"required":["tasks"]}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createTask","summary":"Create a task for the agent to run","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":8000},"title":{"type":"string","maxLength":120},"requester":{"type":"string","maxLength":80},"host":{"type":"string","description":"The id of the Mac to run on (see `listHosts`). Optional: without it the server picks the Mac with the most recent heartbeat."}},"required":["prompt"]}}}},"responses":{"201":{"description":"The task was created and queued.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"host":{"type":"string","description":"The Mac the task was queued for."},"url":{"type":"string","format":"uri"}},"required":["id","status","host","url"]}}}},"400":{"description":"The body failed validation, `host` names no Mac (`unknown host`), or no Mac is set up at all (`no Mac is set up`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tasks/{id}":{"get":{"operationId":"getTask","summary":"Get a single task","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The task.","content":{"application/json":{"schema":{"type":"object","properties":{"task":{"$ref":"#/components/schemas/Task"}},"required":["task"]}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No task with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tasks/{id}/events":{"get":{"operationId":"listTaskEvents","summary":"List a task's step log, in ascending order","description":"Returns the NEWEST `limit` events, ordered oldest-first within that window. `after` is exclusive: pass the `ts` of the last event you already have to poll forward.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"after","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"description":"How many of the newest events to return.","schema":{"type":"integer","default":200,"minimum":1}}],"responses":{"200":{"description":"The newest events after the given timestamp, oldest first.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/Event"}}},"required":["events"]}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No task with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tasks/{id}/messages":{"post":{"operationId":"sendTaskMessage","summary":"Send a follow-up message to a running task","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":4000}},"required":["text"]}}}},"responses":{"202":{"description":"The message was recorded."},"400":{"description":"The body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No task with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tasks/{id}/stop":{"post":{"operationId":"stopTask","summary":"Stop a task","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"The stop request was recorded. `mode` says what happened: `cancelled` (the task is now `cancelled` — it was still queued, or the task's Mac is offline so the stop was applied directly), `requested` (a live host holds it and is winding down), or `noop` (nothing left to stop).","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["cancelled","requested","noop"]}},"required":["mode"]}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No task with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tasks/{id}/continue":{"post":{"operationId":"continueTask","summary":"Tell a task waiting on the owner (`needs_you`) to continue","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"The continue request was recorded."},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No task with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The task is not currently waiting for input.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/permissions/{id}":{"post":{"operationId":"answerPermission","summary":"Allow or deny an open permission request","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"behavior":{"type":"string","enum":["allow","deny"]}},"required":["behavior"]}}}},"responses":{"200":{"description":"The answered permission.","content":{"application/json":{"schema":{"type":"object","properties":{"permission":{"$ref":"#/components/schemas/Permission"}},"required":["permission"]}}}},"400":{"description":"The body failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No permission with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The permission is no longer open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/openapi.json":{"get":{"operationId":"getOpenApiDocument","summary":"This document","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/slack/events":{"post":{"operationId":"slackEvents","summary":"Slack Events API ingress (phase 2, not yet implemented)","security":[],"responses":{"501":{"description":"Slack ingress is phase 2.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/cron/cleanup":{"get":{"operationId":"cleanup","summary":"Trim old screenshots and expire stale permissions","description":"Scans the newest tasks only, so one run's cost is bounded. Nothing schedules this yet — see the README's Web app section.","security":[{"cronSecret":[]}],"parameters":[{"name":"tasks","in":"query","required":false,"description":"How many of the newest tasks to scan. Raise it for a one-off catch-up run; values outside the range are clamped into it.","schema":{"type":"integer","default":200,"minimum":1,"maximum":1000}}],"responses":{"200":{"description":"Counts of what was scanned and cleaned up.","content":{"application/json":{"schema":{"type":"object","properties":{"scanned":{"type":"integer"},"deleted":{"type":"integer"},"expired":{"type":"integer"}},"required":["scanned","deleted","expired"]}}}},"401":{"description":"Missing or invalid cron secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}