Skip to content

REST API ​

Telescreen serves a JSON API under /api/v1. The web app uses the same API, so anything the wall can do is scriptable. An OpenAPI 3.1 document is generated from the running server at /openapi.json.

Base URL and authentication ​

Requests go to the address you use in the browser:

bash
TELESCREEN_URL=https://machine.your-tailnet.ts.net

Every route except /api/v1/health requires authentication:

  • Bearer token. Create one with the CLI: Authorization: Bearer ts_.... Tokens are stored SHA-256 hashed; the plain value is shown once at creation.
  • Network trust. When the server runs with TELESCREEN_TRUST_NETWORK=1, a request with no Authorization header acts as the household. A header that is sent is still verified, so an invalid token returns 401 even in trust mode. Only enable this on a private network.

Send and receive JSON. Request bodies with a JSON content type.

bash
curl -s "$TELESCREEN_URL/api/v1/health"
# {"ok":true,"version":"0.1.0"}

curl -s -H "Authorization: Bearer $TOKEN" "$TELESCREEN_URL/api/v1/household"

Errors ​

Failures use one envelope:

json
{ "error": { "code": "not_found", "message": "Account not found" } }

details is added when the error carries structured information, for example the Zod issues for a validation failure or retryAfterSeconds for a rate limit.

HTTPcodeMeaning
400validationBody, query, or path failed validation; details lists the issues
401unauthorizedMissing, invalid, or revoked token
403forbiddenThe calendar is read-only, or Spotify requires Premium
404not_foundUnknown id, or the module is disabled
409conflictThe event changed at the provider (etag precondition failed)
422unprocessableUnderstood but not actionable: not configured, no_active_device, missing_scope, needs_reconnect
502provider_errorGoogle, Apple, or Spotify failed the request
503unavailableModule not configured, provider unreachable with no cache, or rate limited

Conventions ​

  • Dates in chore and meal endpoints are YYYY-MM-DD in the household time zone.
  • Event instants are ISO 8601 UTC strings. All-day events use YYYY-MM-DD with an exclusive end.
  • Ids are opaque strings. Store and send them as received.
  • Recurring events are expanded server-side (RRULE, EXDATE, overrides). id identifies one instance; eventId identifies the series.
  • There is no pagination: range endpoints take explicit windows.

Health ​

MethodPathPurpose
GET/healthLiveness and version. Public, no token. Returns { ok, version }

Household ​

MethodPathPurpose
GET/householdThe household: id, name, timezone, week start, default theme
PATCH/householdUpdate any of name, timezone, weekStart (0–6), themeDefault (system, light, dark)

Members ​

MethodPathPurpose
GET/membersMembers ordered by sort
POST/membersCreate: name, colorToken, optional initial
PATCH/members/{id}Update name, colour, initial, or sort
DELETE/members/{id}Delete the member

colorToken is one of terracotta, sage, ochre, plum, slate, rose.

Settings ​

MethodPathPurpose
GET/settingsAll settings with defaults applied
PUT/settingsReplace the keys you send; omitted keys keep their value

Settings include weekMode, idleReturnMinutes, syncIntervalMinutes, keepAwake, seasonalDecor, modules (weather, meals, music, travel), defaultWriteCalendarId, weather, musicPins, and travel.

Chores and occurrences ​

MethodPathPurpose
GET/choresChore templates, archived included
POST/choresCreate a chore
PATCH/chores/{id}Update or archive a chore; history is kept
DELETE/chores/{id}Delete a chore and its history
GET/chore-occurrencesOccurrences in a range; materialized on demand
GET/chore-occurrences/progressDone/total per member for one date
POST/chore-occurrences/{id}/completeMark done; body { "completedBy": "mem_..." }
POST/chore-occurrences/{id}/undoReopen a done or skipped occurrence
POST/chore-occurrences/{id}/skipSkip an open occurrence
POST/chore-occurrences/{id}/reassignOverride one occurrence's assignee; body { "memberId": "mem_..." }

Query parameters:

EndpointParameters
GET /chore-occurrencesfrom and to (required, YYYY-MM-DD), member (optional id), carryover=true to include still-open earlier occurrences
GET /chore-occurrences/progressdate (required, YYYY-MM-DD)

Occurrence status is open, done, missed, or skipped. Assignment is fixed, rotating, or either.

bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"completedBy":"mem_2p9q1r2s3t4u5v6w"}' \
  "$TELESCREEN_URL/api/v1/chore-occurrences/occ_5v6w7x8y9z2a3b4c/complete"

Calendar accounts and sync ​

MethodPathPurpose
GET/accountsConnected accounts; credentials are never included
POST/accounts/icloudConnect iCloud: label, appleId, appPassword
POST/accounts/google/startReturn the Google consent URL to open
DELETE/accounts/{id}Disconnect and drop cached calendars and events (204)
POST/accounts/{id}/syncSync one account now
POST/syncSync every account now
GET/sync/status{ lastSyncedAt, hasErrors } for the header indicator

POST /accounts/google/start and POST /music/connect/start return { "authUrl": "..." }. Open that URL in a browser; the flow finishes at the callback routes below, not through the API.

Calendars ​

MethodPathPurpose
GET/calendarsDiscovered calendars with account, colour, writable flag, member mapping
PATCH/calendars/{id}Set enabled and/or memberId (null = Family / Both)

Events ​

MethodPathPurpose
GET/eventsExpanded event instances in a window
POST/eventsCreate an event; writes to the provider first
PATCH/events/{id}Edit an event or one occurrence
DELETE/events/{id}Delete an event or one occurrence (204)
EndpointParameters
GET /eventsstart and end (required ISO instants, end exclusive), members (comma-separated ids; family selects unassigned calendars)
PATCH, DELETE /events/{id}scope=this or scope=all (default all)

Create body: calendarId, title, start, end, allDay, optional location, notes, and repeat (none, daily, weekdays, weekly, monthly, yearly, custom).

bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "$TELESCREEN_URL/api/v1/events?start=2026-11-14T00:00:00Z&end=2026-11-21T00:00:00Z&members=family"

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"calendarId":"cal_8f3k2m9p0q1r2s3t","title":"Dentist","start":"2026-11-16T09:00:00Z","end":"2026-11-16T10:00:00Z","allDay":false}' \
  "$TELESCREEN_URL/api/v1/events"

A read-only calendar returns 403. A 409 conflict means the event changed at the provider after the last sync; read it again and retry.

Music ​

MethodPathPurpose
GET/music/connectionConnected, configured, display name, status
POST/music/connect/startReturn the Spotify consent URL (422 when unconfigured)
DELETE/music/connectionForget the stored token (204)
GET/music/now-playingTrack, artists, album art, progress, and active device
GET/music/devicesAvailable Spotify Connect devices
POST/music/playResume playback (204)
POST/music/pausePause (204)
POST/music/nextNext track (204)
POST/music/previousPrevious track (204)
PUT/music/volumeBody { "percent": 0-100 } (204)
PUT/music/deviceBody { "deviceId": "..." }; transfers playback (204)
GET/music/recentRecently played albums (max 12)
GET/music/playlistsUp to 50 playlists, preceded by Liked Songs
POST/music/play-contextStart a spotify: URI on the active device (204)

Playback routes return 422 not_connected when no Spotify account is connected, 422 needs_reconnect when the stored token was rejected, 422 no_active_device when Spotify has no active device, and 403 premium_required without Premium. /music/recent and /music/playlists return 422 missing_scope until the account is reconnected. track is null when nothing is playing.

Meals ​

MethodPathPurpose
GET/meals/currentThis week, tonight's meal, cache timestamp, stale flag
GET/meals/planPlanned days in a range (start and end, YYYY-MM-DD, end exclusive, max 62 days)
GET/meals/boardLike /meals/plan plus recipe photo, time, cuisine, and leftover provenance
GET/meals/kitchenShopping list, pantry gaps, and expiring food
GET/meals/statusConfiguration only, no network call
POST/meals/testFetch the current plan once and report the result

Responses carry fetchedAt and stale. stale: true means PantryPal was unreachable and the last good response is being served. A disabled module returns 404; missing configuration or an unreachable PantryPal with no cache returns 503.

Weather ​

MethodPathPurpose
GET/weatherCurrent conditions, today, a five-day forecast, and the next 24 hours

Open-Meteo needs no key. The location and units come from settings, and the household time zone is used. Cached for 15 minutes, with stale: true on upstream failure. Returns 503 when the module is disabled, no location is set, or upstream fails with no cache.

Travel ​

MethodPathPurpose
GET/travelDriving time from home to every saved destination
GET/travel/statusModule and API-key status, no network call

Requires GOOGLE_MAPS_API_KEY, the Travel module, and a home address. Cached for 10 minutes, with stale: true on failure. Returns 503 when unconfigured or disabled.

Routes outside the API ​

Two OAuth redirect targets are mounted at the root, not under /api/v1, and are not part of the OpenAPI document:

MethodPathPurpose
GET/auth/google/callbackFinish Google OAuth; redirects to /settings
GET/auth/spotify/callbackFinish Spotify OAuth; redirects to /settings

Browsers arrive there without a token. Each uses the single-use state nonce issued by the matching start endpoint as proof, and renders an HTML page on failure.

OpenAPI ​

GET /openapi.json returns the full 3.1 document, including schemas, tags, and the bearer security scheme. It is public, so a client generator can be pointed at a running instance:

bash
curl -s "$TELESCREEN_URL/openapi.json" > telescreen-openapi.json

The web app's TypeScript types are generated from this document during development; see Local setup.

MIT licensed