The HTTP bridge runs alongside the daemon when [bridge] enabled = true
in mxr.toml. It exposes the same IPC contract the TUI uses, but over
HTTP — so desktop apps, mobile clients, agent runners, and your own
shell scripts all talk to the same daemon through one stable surface.
The bridge serves an OpenAPI 3.1 spec at
http://127.0.0.1:42829/api/v1/openapi.json (port and host configurable
in [bridge]). The desktop app generates its TypeScript client from
this spec — you can do the same for any language with openapi-generator
or openapi-typescript.
Every request needs Authorization: Bearer $MXR_TOKEN. WebSocket
clients can also pass the token via the Sec-WebSocket-Protocol
subprotocol or as a ?token= query string.
The SPA served by mxr web doesn’t ask the user to paste a token.
GET /api/v1/auth/local-token is an unauthenticated endpoint that
returns the bridge token to callers whose TCP peer is a loopback IP.
curl http://127.0.0.1: $MXR_PORT /api/v1/auth/local-token
# → {"token":"<uuid>","source":"local-handshake"}
The endpoint returns 404 (not 401) when:
[bridge].auto_local_token = false — operator opted out.
The connecting peer is not a loopback address — the bridge is bound to a non-loopback interface and the caller is on a different machine.
This lets the local SPA self-authenticate while keeping the same
strict bearer-handshake story for remote callers.
curl -H " Authorization: Bearer $MXR_TOKEN " " $MXR_BASE /api/v1/admin/status "
Response:
"accounts" : [ " me@example.com " ],
Method Path Purpose GET/api/v1/healthUnauthenticated liveness probe GET/api/v1/openapi.jsonOpenAPI 3.1 spec GET/api/v1/docsSwagger UI GET/api/v1/eventsWebSocket — daemon events stream GET/api/v1/desktop/shellDesktop manifest (sidebar + commands)
Method Path Purpose GET/admin/statusStatus snapshot (uptime, pid, accounts, sync) GET/admin/diagnosticsDoctorReport with findings + remediation GET/admin/diagnostics/bug-reportBundled bug report (Markdown) GET/admin/eventsRecent daemon events (paged) GET/admin/logsRecent log lines (paged) POST/admin/pingLiveness round-trip POST/admin/shutdownGraceful daemon shutdown
Method Path Purpose GET/mail/mailboxBrowse the inbox or any lens GET/mail/searchRun a Tantivy search GET/mail/threads/{id}Full thread payload (messages + bodies) GET/mail/threads/{id}/exportMarkdown / JSON export GET/mail/draftsList drafts GET/mail/snoozedList snoozed messages GET/mail/countCount messages matching a query GET/mail/sync/statusPer-account sync state
curl -G -H " Authorization: Bearer $MXR_TOKEN " \
" $MXR_BASE /api/v1/mail/search " \
--data-urlencode ' q=is:unread from:billing ' \
--data-urlencode ' mode=lexical ' \
--data-urlencode ' limit=20 '
All mutations accept message_ids: string[] in the JSON body unless
noted. They emit a MutationCompleted event over the WebSocket so
clients can reconcile optimistically.
Method Path Purpose POST/mail/mutations/archiveRemove from inbox POST/mail/mutations/trashMove to trash POST/mail/mutations/spamMark as spam POST/mail/mutations/starStar / unstar ({starred: bool}) POST/mail/mutations/readMark read / unread ({read: bool}) POST/mail/mutations/read-and-archiveCombined POST/mail/mutations/labelsModify labels ({add, remove}) POST/mail/mutations/moveMove to another label POST/mail/mutations/undoUndo via mutation_id POST/mail/syncTrigger sync POST/mail/snoozed/{id}/wakeForce-unsnooze GET/mail/actions/snooze/presetsAvailable snooze presets POST/mail/actions/snoozeSnooze messages POST/mail/actions/unsubscribeUnsubscribe from list mail
curl -X POST -H " Authorization: Bearer $MXR_TOKEN " \
-H " Content-Type: application/json " \
-d ' {"message_ids":["..."], "starred":true} ' \
" $MXR_BASE /api/v1/mail/mutations/star "
The “delight features” land here. Each maps 1-1 to its CLI/TUI counterpart.
Method Path Purpose GET/mail/reply-laterList flagged messages POST/mail/reply-later/{message_id}Set/clear flag ({flag: bool})
Method Path Purpose POST/mail/remindersSchedule ({sent_message_id, remind_at}) DELETE/mail/reminders/{message_id}Cancel
Method Path Purpose POST/mail/scheduled-sends{draft_id, send_at}DELETE/mail/scheduled-sends/{draft_id}Cancel pending send
Method Path Purpose GET/mail/snippetsList POST/mail/snippetsCreate / update ({name, body, vars}) DELETE/mail/snippets/{name}Remove
Method Path Purpose GET/mail/sender?account_id=...&email=...Per-sender aggregates
Method Path Purpose GET/mail/screener/queue?account_id=...&limit=...Senders awaiting decision GET/mail/screener/decisions?account_id=...All existing decisions POST/mail/screener/decisions{account_id, sender_email, disposition, route_label?}DELETE/mail/screener/decisionsClear ({account_id, sender_email})
disposition is one of: allow, deny, feed, paper_trail, unknown.
Method Path Purpose POST/mail/threads/{thread_id}/summarize2-3 sentence thread summary POST/mail/threads/draft-assist{thread_id, instruction} → suggested reply body
curl -X POST -H " Authorization: Bearer $MXR_TOKEN " \
" $MXR_BASE /api/v1/mail/threads/THREAD_ID/summarize "
"text" : " Alice asked Bob to confirm the launch checklist; he hasn't replied since Monday. " ,
"model" : " qwen2.5:3b-instruct "
The desktop and any other interactive client open a compose session
that the daemon owns. The state (frontmatter + body + attachments)
lives server-side until you send/save/discard.
Method Path Purpose POST/mail/compose/sessionOpen new / reply / forward POST/mail/compose/session/refreshRefetch latest state POST/mail/compose/session/restoreResume saved draft POST/mail/compose/session/updateSave current edits POST/mail/compose/session/sendSend (calls provider) POST/mail/compose/session/saveSave to drafts table only POST/mail/compose/session/discardThrow away
Method Path Purpose POST/mail/attachments/openMaterialise attachment to a tempfile POST/mail/attachments/downloadStream the attachment
Method Path Purpose POST/mail/labels/create{name}POST/mail/labels/rename{from, to}POST/mail/labels/delete{name}
These are the “platform” features — saved searches, rules, account
management, analytics, LLM status, semantic search. Available even
without an active inbox.
Method Path Purpose GET/platform/rulesList GET/platform/rules/detail?id=...Full detail GET/platform/rules/form?id=...Editor-friendly form payload GET/platform/rules/history?id=...Change history GET/platform/rules/dry-run?id=...&since=...Preview matches POST/platform/rules/upsertCreate / update POST/platform/rules/upsert-formFrom the form payload POST/platform/rules/delete{id}
Method Path Purpose GET/platform/saved-searchesList POST/platform/saved-searches/create{name, query, mode}POST/platform/saved-searches/delete{name}POST/platform/saved-searches/run{name}
Method Path Purpose GET/platform/accountsRuntime inventory (with health) GET/platform/accounts/configConfig-backed account list POST/platform/accounts/testTest credentials POST/platform/accounts/upsertAdd / update an account POST/platform/accounts/default{key} set defaultDELETE/platform/accounts/{key}Remove POST/platform/accounts/{key}/disableSoft-disable GET/platform/accounts/{id}/addressesAliases for an account POST/platform/accounts/{id}/addressesAdd alias POST/platform/accounts/{id}/addresses/removeRemove alias POST/platform/accounts/{id}/addresses/primarySet primary alias
The daemon owns OAuth flows so the renderer never sees a refresh token.
Method Path Purpose POST/platform/auth/sessions/startBegin an OAuth flow GET/platform/auth/sessions/{id}Poll progress POST/platform/auth/sessions/{id}/cancelAbort POST/platform/auth/sessions/{id}/completeWrap up after callback
Method Path Purpose GET/platform/llm/configCurrent [llm] config, without secrets POST/platform/llm/configUpdate [llm] config and reload provider GET/platform/llm/statusRuntime LLM provider + model status
Method Path Purpose GET/platform/semantic/statusIndex health + active profile POST/platform/semantic/enableActivate POST/platform/semantic/reindexRebuild POST/platform/semantic/profiles/install{profile} (e.g. bge-small-en-v1.5)POST/platform/semantic/profiles/useSwitch active profile
Method Path Purpose GET/platform/analytics/wrappedYear-in-review GET/platform/analytics/storage-breakdownDisk by sender/mimetype/label GET/platform/analytics/largest-messagesHeaviest messages GET/platform/analytics/stale-threads”Whose turn is it?” GET/platform/analytics/contact-asymmetryReply-imbalance ranking GET/platform/analytics/contact-decayGoing-cold relationships GET/platform/analytics/response-timeReply-latency percentiles POST/platform/analytics/refresh-contactsMaterialise contacts table POST/platform/analytics/rebuildRebuild analytics views
Method Path Purpose GET/platform/subscriptionsNewsletter inventory + ROI
Connect a WebSocket to /api/v1/events and you’ll receive a JSON line
per daemon event. The TypeScript shapes are in
apps/desktop/src/shared/api.generated.ts under DaemonEvent. Common
ones:
MutationCompleted — your last mutation landed (or rolled back)
SyncStarted / SyncFinished
ReminderTriggered — auto-reminder fired
ScheduledSendFlushed — a Send Later draft just went out
IndexBootstrapped — Tantivy completed a startup repair
# Quick subscribe via websocat
websocat -H " Authorization: Bearer $MXR_TOKEN " \
ws://127.0.0.1:7777/api/v1/events
npx openapi-typescript $MXR_BASE /api/v1/openapi.json -o src/api.generated.ts
openapi-generator generate -i $MXR_BASE /api/v1/openapi.json -g python -o ./mxr-py
openapi-generator generate -i $MXR_BASE /api/v1/openapi.json -g rust -o ./mxr-rs