Skip to content

HTTP Bridge

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.

Terminal window
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.

Terminal window
curl -H "Authorization: Bearer $MXR_TOKEN" "$MXR_BASE/api/v1/admin/status"

Response:

{
"uptime_secs": 1822,
"daemon_pid": 4242,
"accounts": ["me@example.com"],
"total_messages": 12044,
"sync_statuses": [...]
}
MethodPathPurpose
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)

/api/v1/admin/* — Daemon health and operations

Section titled “/api/v1/admin/* — Daemon health and operations”
MethodPathPurpose
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
MethodPathPurpose
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
Terminal window
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.

MethodPathPurpose
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
Terminal window
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.

MethodPathPurpose
GET/mail/reply-laterList flagged messages
POST/mail/reply-later/{message_id}Set/clear flag ({flag: bool})
MethodPathPurpose
POST/mail/remindersSchedule ({sent_message_id, remind_at})
DELETE/mail/reminders/{message_id}Cancel
MethodPathPurpose
POST/mail/scheduled-sends{draft_id, send_at}
DELETE/mail/scheduled-sends/{draft_id}Cancel pending send
MethodPathPurpose
GET/mail/snippetsList
POST/mail/snippetsCreate / update ({name, body, vars})
DELETE/mail/snippets/{name}Remove
MethodPathPurpose
GET/mail/sender?account_id=...&email=...Per-sender aggregates
MethodPathPurpose
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.

MethodPathPurpose
POST/mail/threads/{thread_id}/summarize2-3 sentence thread summary
POST/mail/threads/draft-assist{thread_id, instruction} → suggested reply body
Terminal window
curl -X POST -H "Authorization: Bearer $MXR_TOKEN" \
"$MXR_BASE/api/v1/mail/threads/THREAD_ID/summarize"
{
"kind": "ThreadSummary",
"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.

MethodPathPurpose
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
MethodPathPurpose
POST/mail/attachments/openMaterialise attachment to a tempfile
POST/mail/attachments/downloadStream the attachment
MethodPathPurpose
POST/mail/labels/create{name}
POST/mail/labels/rename{from, to}
POST/mail/labels/delete{name}

/api/v1/platform/* — Rules, accounts, LLM, semantic, analytics

Section titled “/api/v1/platform/* — Rules, accounts, LLM, semantic, analytics”

These are the “platform” features — saved searches, rules, account management, analytics, LLM status, semantic search. Available even without an active inbox.

MethodPathPurpose
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}
MethodPathPurpose
GET/platform/saved-searchesList
POST/platform/saved-searches/create{name, query, mode}
POST/platform/saved-searches/delete{name}
POST/platform/saved-searches/run{name}
MethodPathPurpose
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 default
DELETE/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.

MethodPathPurpose
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
MethodPathPurpose
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
MethodPathPurpose
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
MethodPathPurpose
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
MethodPathPurpose
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
Terminal window
# Quick subscribe via websocat
websocat -H "Authorization: Bearer $MXR_TOKEN" \
ws://127.0.0.1:7777/api/v1/events
Terminal window
# TypeScript
npx openapi-typescript $MXR_BASE/api/v1/openapi.json -o src/api.generated.ts
# Python
openapi-generator generate -i $MXR_BASE/api/v1/openapi.json -g python -o ./mxr-py
# Rust
openapi-generator generate -i $MXR_BASE/api/v1/openapi.json -g rust -o ./mxr-rs
  • CLI reference — same surface, terminal-friendly.
  • Recipes — composing the bridge with curl/jq/agents.
  • Desktop app — first-party consumer of this bridge.
  • For agents — boundaries when an LLM drives the API.