Skip to content

HTTP Bridge

The HTTP bridge runs alongside the daemon when [bridge] enabled = true in the active config.toml. It exposes the same IPC contract the TUI uses, but over HTTP — so the web app, 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://mxr.localhost:42829/api/v1/openapi.json (port and host configurable in [bridge]). The spec is authenticated like the rest of the bridge API. The web app generates its TypeScript client from this spec — you can do the same for any language with openapi-generator or openapi-typescript.

Every request except /api/v1/health, /api/v1/auth/local-token, and /api/v1/i18n needs Authorization: Bearer $MXR_TOKEN. OpenAPI and Swagger UI are not special-cased. WebSocket clients can also pass the token via the Sec-WebSocket-Protocol subprotocol or as a ?token= query string.

A plain browser location bar cannot attach an Authorization header. For generated clients, dump /api/v1/openapi.json with curl first. For normal browser use, open the first-party web app with mxr web.

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://mxr.localhost:$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,
"instance": "mxr",
"is_demo": false,
"accounts": ["me@example.com"],
"total_messages": 12044,
"sync_statuses": [...]
}
MethodPathAuthPurpose
GET/api/v1/healthNoLiveness probe
GET/api/v1/auth/local-tokenLoopback peer onlySame-machine token bootstrap
GET/api/v1/i18nNoLocale strings for the first-party web app
GET/api/v1/openapi.jsonYesOpenAPI 3.1 spec
GET/api/v1/docsYesSwagger UI
GET/api/v1/eventsYesWebSocket — daemon events stream
GET/api/v1/client/shellYesClient shell data (sidebar + status)

/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/messages/{message_id}/bodyFetch one message body
GET/mail/messages/{message_id}/html-imagesHTML-linked image asset list
GET/mail/messages/{message_id}/headersRaw RFC 5322 headers
GET/mail/invitesList detected calendar invites (?limit=200)
GET/mail/snoozedList snoozed messages
GET/mail/deliveriesList tracked deliveries (?filter=active|delivered|all|dismissed)
GET/mail/deliveries/{id}One delivery + its source message ids
GET/mail/countCount messages matching a query
GET/mail/sync/statusPer-account sync state
POST/mail/export-searchExport all threads matching a search
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
POST/mail/actions/invite/replyDry-run or send a calendar invite RSVP
POST/mail/deliveries/scanBackfill scan ({since_days, dry_run})
POST/mail/deliveries/{id}/resolveMark a delivery delivered/done
POST/mail/deliveries/{id}/dismissHide a false positive
POST/mail/messages/{message_id}/flagsSet message flags (bitmask in body)
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"

List detected calendar invites:

Terminal window
curl -G -H "Authorization: Bearer $MXR_TOKEN" \
"$MXR_BASE/api/v1/mail/invites" \
--data-urlencode 'limit=50'

Dry-run calendar replies before sending the iMIP METHOD:REPLY email:

Terminal window
curl -X POST -H "Authorization: Bearer $MXR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message_id":"MESSAGE_ID","action":"accept","dry_run":true}' \
"$MXR_BASE/api/v1/mail/actions/invite/reply"

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 plus recent messages from that sender
GET/mail/contacts/autocomplete?q=...&limit=...Prefix-search known senders (filters by email or display name)

The sender response is SenderProfile { profile }. When present, profile includes recent_messages: the newest messages from that sender with message_id, thread_id, subject, snippet, date, direction, and an attachment-present flag. Clients use this to render “Other emails from sender” and deep-link directly into the matching thread.

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}/summarizeConcise Markdown thread summary + next steps
POST/mail/threads/draft-assist{thread_id, instruction} → suggested reply body plus model/humanizer/voice metadata
POST/mail/drafts/newStart an LLM-backed draft from a prompt
POST/mail/drafts/refineRefine draft body with model knobs
POST/mail/humanizer/scoreScore text against your voice profile
POST/mail/humanizer/rewriteRewrite toward human-like / on-voice output
MethodPathPurpose
GET/mail/relationship?account_id=...&email=...Per-contact relationship profile
POST/mail/relationship/rebuildJSON {account_id, email} — rebuild relationship summaries
GET/mail/commitments?account_id=...&email=...&status=...List open commitments
POST/mail/commitments/{commitment_id}/resolveMark a commitment resolved
MethodPathPurpose
GET/mail/drafts/orphanedMid-send / stuck drafts
POST/mail/drafts/save-localPersist a draft row without compose session
POST/mail/drafts/{draft_id}/reset-orphanRecover an orphaned send
POST/mail/drafts/{draft_id}/send-storedSend a stored draft by id
DELETE/mail/drafts/{draft_id}/storedDelete stored draft
MethodPathPurpose
GET/mail/signaturesList
POST/mail/signaturesCreate or update
DELETE/mail/signatures/{name}Remove
GET/mail/signature-defaultsDefaults per context
POST/mail/signatures/defaultSet default
POST/mail/signatures/default/clearClear default
POST/mail/signatures/resolveResolve signature for compose context
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/attachmentAttach an uploaded file to the session
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/update{name, new_name?, query?, search_mode?, sort?, icon?, position?} — patch by current name. icon doubles as a CSS color tag; position < 0 pins.
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/authorize{account, reauthorize}AuthorizeAccountConfig (OAuth / credential handoff)
POST/platform/accounts/repairRepair keychain / stored credentials for a config
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/backfillBackfill chunks / embeddings workload
POST/platform/semantic/profiles/install{profile} (e.g. bge-small-en-v1.5)
POST/platform/semantic/profiles/useSwitch active profile
MethodPathPurpose
GET/platform/voiceCached user voice profile used by humanizer / draft assist
POST/platform/voice/rebuildRecompute voice profile from sent mail
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/web/src/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://mxr.localhost:$MXR_PORT/api/v1/events
Terminal window
curl -H "Authorization: Bearer $MXR_TOKEN" \
"$MXR_BASE/api/v1/openapi.json" > spec.json
# TypeScript
npx openapi-typescript spec.json -o src/api.generated.ts
# Python
openapi-generator generate -i spec.json -g python -o ./mxr-py
# Rust
openapi-generator generate -i spec.json -g rust -o ./mxr-rs
  • CLI reference — same surface, terminal-friendly.
  • Recipes — composing the bridge with curl/jq/agents.
  • Web app — first-party browser client for this bridge.
  • For agents — boundaries when an LLM drives the API.
  • Contributors: see docs/guides/http-bridge.md in the repo for the internal architecture and security model.