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.
On this page
/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?} |
| 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
/eventsbecause 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.
This page also ships inside the app, at /guide โ so your own instance always serves the guide for the version you are running, with the internet unplugged. Get Latch ยท Something wrong here? Tell me.