Files
wristcode/docs/api.md
T
gib 21215210e3
Gateway / check (push) Successful in 8s
Gateway / image (push) Successful in 21s
Contract: pass approval option warnings, omit threadId on host pushes
2026-11-01 02:36:44 -05:00

4.9 KiB

Gateway API

The watch app talks to the gateway over HTTPS with JSON bodies. The TypeScript types in 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 <deviceToken>. Timestamps are ISO 8601 strings in UTC.

Errors return a non-2xx status with { "error": "<message for a person>" }.

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": "<hex>", "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": []
}

An option may also carry warning, a short note about a risky choice such as "Applies to every thread". 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": { "<questionId>": "<value>" } }
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 (absent on host pushes), 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] }