# Gateway API The watch app talks to the gateway over HTTPS with JSON bodies. The TypeScript types in [`gateway/src/contract.ts`](../gateway/src/contract.ts) are the source of truth, and the watch mirrors them in `watch/WristCode/API/Models.swift`. Change both together. The base URL is `https://wristcode.gbrown.org`. Every route except `GET /v1/health` and `POST /v1/pair` needs `Authorization: Bearer `. Timestamps are ISO 8601 strings in UTC. Errors return a non-2xx status with `{ "error": "" }`. | Status | Meaning | | --- | --- | | 400 | The request body is wrong | | 401 | The device token is missing or revoked. Show the pairing screen | | 404 | Unknown host, thread, or project | | 409 | The request is stale, for example an approval that was already answered | | 429 | Too many pairing attempts | | 502 | The T3 server rejected the call. The message says why | | 503 | That host is offline | ## Pairing `wristcode pair` on the gateway prints a 6 digit code that is good for 5 minutes. The watch sends it once and stores the token in its Keychain. ``` POST /v1/pair { "code": "482913", "deviceName": "Gabriel's Apple Watch" } 200 { "deviceId": "9b0c...", "deviceToken": "wc_..." } ``` After pairing, and again whenever APNs hands the watch a new token: ``` PUT /v1/device/push { "apnsToken": "", "apnsEnvironment": "sandbox" } 204 ``` Debug builds use `sandbox`, TestFlight and App Store builds use `production`. ## Feed ``` GET /v1/feed 200 { "needsYou": [FeedItem], "working": [FeedItem], "recent": [FeedItem], "hosts": [{ "id": "...", "label": "Mac mini", "status": "online", "error": null }] } ``` `needsYou` holds threads waiting for an approval or an answer, oldest first. `working` holds running threads, most recently updated first. `recent` holds threads that completed or failed in the last 24 hours, at most 10. Host `status` is one of `connecting`, `online`, `offline`, `auth_invalid`, or `incompatible`. ``` FeedItem { "hostId": "...", "hostLabel": "Mac mini", "threadId": "...", "projectId": "...", "projectTitle": "bill-tracker", "title": "Receipt OCR", "phase": "waiting_for_approval", "updatedAt": "2026-11-01T09:41:00.000Z", "lastMessage": "Parser and tests are written. I want to run the suite.", "request": PendingRequest | null } ``` `phase` is one of `starting`, `running`, `waiting_for_approval`, `waiting_for_input`, `completed`, or `failed`. `lastMessage` is the agent's latest message trimmed to about 280 characters. It can be null for a moment after a thread changes. ``` PendingRequest { "id": "...", "kind": "command", "prompt": "bun test --filter ocr", "reason": null, "options": [{ "decision": "accept", "label": "Allow" }, { "decision": "decline", "label": "Deny" }], "questions": [] } ``` For questions (`kind: "user_input"`), `options` is empty and `questions` lists `{ id, header, question, options: [{ label, value }], allowCustomAnswer }`. ## Thread ``` GET /v1/threads/:hostId/:threadId 200 { "item": FeedItem, "messages": [Message], "canStop": true } Message { "id": "...", "role": "assistant", "text": "...", "createdAt": "..." } ``` `messages` holds the last 20, oldest first, each trimmed to 2000 characters. ``` POST /v1/threads/:hostId/:threadId/messages { "text": "Yes, commit it" } 202 ``` A message to a busy thread waits until the current turn ends. Nothing is interrupted. ``` POST /v1/threads/:hostId/:threadId/respond { "requestId": "...", "decision": "accept" } or { "requestId": "...", "answers": { "": "" } } 202 ``` ``` POST /v1/threads/:hostId/:threadId/stop 202 ``` ## New thread ``` GET /v1/projects 200 { "hosts": [{ "id": "...", "label": "Mac mini", "status": "online", "error": null, "projects": [{ "id": "...", "title": "wristcode", "model": "claude-opus-5-5" }] }] } ``` A project with `"model": null` has no default model, so the watch should not offer it. ``` POST /v1/threads { "hostId": "...", "projectId": "...", "text": "Add a complication for the waiting count" } 201 { "hostId": "...", "threadId": "..." } ``` The new thread uses the project's default model, or the host's, and the host's default runtime mode. ## Notifications The gateway pushes when a thread starts waiting for you, finishes, or fails. Each push has an `aps.category` and the custom keys `hostId`, `threadId`, and, for approvals and questions, `requestId`. | Category | When | Actions the watch registers | | --- | --- | --- | | `approval` | A thread needs an approval | Allow (`accept`), Deny (`decline`) | | `input` | A thread asked a question | Open | | `finished` | A turn completed | Reply (text input), Open | | `failed` | A turn failed | Open | | `host` | A host's T3 token expires within 7 days, or was rejected | none | ## Health ``` GET /v1/health 200 { "ok": true, "hosts": [HostSummary] } ```