🐾 Latch ?

Guide β€Ί Self-hosting

API, Shortcuts and bots

A bearer-token JSON API for phone Shortcuts and local bots β€” off until you enable it, every write stamped with who did it, and deliberately unable to pull your journal in bulk.

/api/v1 is for things that are not a browser: an iPhone Shortcut that logs a photo from the share sheet, a local bot that spins the wheel, an AI Handler acting through the same doors you use. It is off by default.

Enabling

Set LATCH_API_TOKEN in .env and recreate the container. Empty means every /api/v1 route answers 503 β€” the API fails closed, never open. Send the token as a bearer:

Authorization: Bearer <token>
X-Latch-Actor: shortcut

X-Latch-Actor is one of user, api, ai, shortcut (anything else is recorded as api). It is written on every event, so the log says who did what.

The token can read your journal

/api/v1/handler/entries returns reflection and check-in bodies by design. Treat the token as a LAN-only secret: a password manager, a Shortcut on your own phone, a bot on your own box. Never anything that leaves the house.

Timing: at

Every write takes an optional at β€” epoch seconds β€” for something that already happened. The same rules as the web's when field apply (the retroactive log): never the future, never before the lock it lands in, marked logged later if it is more than two minutes old.

Routes

Method Route What
GET /state the clock (status, remaining, end_at, frozen, hidden…) and lifetime totals. The session note is omitted.
POST /session/lock {seconds β‰₯ 60, note?, hidden?, at?}
POST /session/adjust {seconds (Β±), reason?, at?}
POST /session/freeze {minutes? (0 = until unfrozen), reason?, at?}
POST /session/unfreeze {at?}
POST /session/unlock {force?} β€” force: true is the emergency key
GET /session/undo/candidate what undo would hit and, if it cannot, why β€” the same text the UI shows
POST /session/undo {reason?}
POST /note {text, at?}
POST /media/photo multipart photo, caption?, at? β€” a photo check-in. Returns ids and the clock, never the caption
GET /events ?limit=&module= β€” whitelisted payloads (no prose)
POST /wheel/spin spin as the calling actor
GET /wheel/segments the wheel
GET /tasks {open, library}
POST /tasks/assign {task_id?, due_hours?} β€” omit task_id for random
POST /tasks/{id}/complete multipart photo?, note?, at?
POST /tasks/{id}/fail form reason?, at?
GET /handler/today what is due β€” facts only
GET /handler/entries ?kind=&limit= β€” bodies included, by design
POST /handler/reflect {body, prompt_id?, at?}
GET /habits every habit with today's count, this week against its target, streak, week seconds, and the running timer if any β€” facts only, never a log's note
POST /habits/log {slug, minutes?, at?} β€” minutes blank is a check-off. at is when it ended; more than two days back counts and earns no XP
POST /habits/start {slug} β€” start the timer (one at a time)
POST /habits/stop {discard?} β€” stop it and log the elapsed minutes; discard: true writes nothing
POST /handler/reflect/voice multipart audio, duration_seconds, prompt_id?, note?, at?
POST /handler/checkin {scores: {headspace, obedience, mood, denial, energy}, note?, prompt_id?, at?}
POST /handler/recite {kind: mantra\|affirmation, prompt_id, at?}
POST /handler/meditation {minutes, prompt_id?, note?, at?}

That table is the whole surface. A test in the repository holds /api/v1 to exactly this allowlist in both directions β€” a new route fails the suite until it is written down.

Errors come back as {"error": "…"} with a 4xx, using the same sentences the web UI shows.

What is deliberately not on the API

  • Export. Every /export/* route is behind the reverse proxy's login, not the token.
  • Prose on the event feed. Excerpts, scores, notes, reasons and captions are absent from /events because they are not in the whitelist.
  • A Swagger page or /openapi.json. Both would beacon to a CDN on every visit and publish the route list without a token. This page is the documentation.

Examples

Lock for four hours:

curl -s -X POST https://latch.your.lan/api/v1/session/lock \
  -H "Authorization: Bearer $LATCH_API_TOKEN" -H "X-Latch-Actor: shortcut" \
  -H "Content-Type: application/json" -d '{"seconds": 14400}'

A photo check-in from an iPhone Shortcut (Receive images from Share Sheet β†’ Convert Image to JPEG β†’ Get Contents of URL, method POST, form body, field photo = the image):

POST /api/v1/media/photo   (multipart)  photo=<jpeg>  caption=""

A spoken reflection from Record Audio β†’ same pattern with field audio and duration_seconds β€” the server refuses a recording whose length it cannot establish.

Ask the Handler what is due before nagging yourself:

curl -s https://latch.your.lan/api/v1/handler/today -H "Authorization: Bearer $LATCH_API_TOKEN"

Bots and the AI Handler

The API is the door every future bot uses: the Matrix bot (reminders, reflections and commands outside the web UI) and the AI Handler persona (a local model, acting as actor=ai, rate-limited, every action an event). Both are on the roadmap. Whatever you build, build it as a client of this API rather than a second writer to the database β€” that is what keeps the clock derived and the log honest.

Habits

The habits routes exist for one thing: logging from wherever you are without opening the app. An iPhone Shortcut on the Home Screen β€” Guitar, 30 min β€” is two actions:

  1. Get Contents of URL β€” POST https://<your latch>/api/v1/habits/log, headers Authorization: Bearer <token> and X-Latch-Actor: shortcut, JSON body {"slug": "guitar", "minutes": 30}.
  2. Show Result β€” or nothing.

Ask for the minutes with Ask for Input if you want one Shortcut per habit rather than per length. An Automation can fire it when a practice app closes, or when you leave a location. The at field takes epoch seconds for something that already happened.

If the app you practise with already keeps a calendar, you may not need a Shortcut at all β€” see Habits β†’ Reading another app's calendar.

This page is docs/api.md in the repo. Spotted a mistake? Tell us how to report it β†’