Web Server
Overview
msgvault serve starts an HTTP server that exposes your local email archive over a REST API. It optionally runs a background sync scheduler to keep accounts up to date on a cron-based schedule.
The API queries the same SQLite database and attachment store as the CLI and TUI. Keyword search and ordinary archive reads stay local. If vector search is enabled, semantic and hybrid search also call the embedding endpoint configured in [vector.embeddings]. The server is designed for local integrations, dashboards, and automation scripts.
Quick Start
Add a [server] section to your config.toml:
[server]api_port = 8080api_key = "your-secret-key"Start the server:
msgvault serveTest connectivity:
# Health check (no auth required)curl http://localhost:8080/health
# Archive stats (auth required)curl -H "Authorization: Bearer your-secret-key" http://localhost:8080/api/v1/statsAuthentication
All endpoints except /health require authentication when api_key is set in your config. Three authentication methods are supported:
| Method | Header | Example |
|---|---|---|
| Bearer token | Authorization: Bearer <key> | Authorization: Bearer my-secret |
| API key header | X-API-Key: <key> | X-API-Key: my-secret |
| Plain auth header | Authorization: <key> | Authorization: my-secret |
If no api_key is configured, authentication is not required regardless of bind address. The separate allow_insecure / security validation prevents starting without an API key on non-loopback addresses.
API Endpoints
GET /health
Health check endpoint. Does not require authentication.
Response:
{"status": "ok"}GET /api/v1/stats
Archive statistics. When vector search is configured on the server,
the response also includes a vector_search sub-object describing
the state of the index.
Response (vector search disabled):
{ "total_messages": 142857, "total_threads": 48293, "total_accounts": 2, "total_labels": 47, "total_attachments": 31204, "database_size_bytes": 8589934592}Response (vector search enabled):
{ "total_messages": 142857, "total_threads": 48293, "total_accounts": 2, "total_labels": 47, "total_attachments": 31204, "database_size_bytes": 8589934592, "vector_search": { "enabled": true, "active_generation": { "id": 3, "model": "nomic-embed-text-v1.5", "dimension": 768, "fingerprint": "nomic-embed-text-v1.5:768:p1-111111:c32768:e1", "state": "active", "activated_at": "2026-04-18T15:12:33Z", "message_count": 142820 }, "building_generation": { "id": 4, "model": "nomic-embed-text-v2", "dimension": 768, "started_at": "2026-04-19T09:02:10Z", "progress": { "done": 8200, "total": 142857 } }, "pending_embeddings_total": 134657 }}active_generation is always present in the object (null until the
first build completes). building_generation is omitted when no
rebuild is in flight. pending_embeddings_total is the sum of rows
still pending across the active and building generations. See
Vector Search for the end-to-end workflow.
GET /api/v1/messages
Paginated message list.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
page_size | int | 20 | Results per page |
Response:
{ "total": 142857, "page": 1, "page_size": 20, "messages": [ { "id": 12345, "subject": "Q4 Planning", "sent_at": "2024-10-15T09:30:00Z", "snippet": "Here's the draft for Q4...", "labels": ["INBOX", "IMPORTANT"], "has_attachments": true, "size_bytes": 52480 } ]}GET /api/v1/messages/{id}
Full message details including body and attachment metadata.
Response:
{ "id": 12345, "subject": "Q4 Planning", "sent_at": "2024-10-15T09:30:00Z", "snippet": "Here's the draft for Q4...", "labels": ["INBOX", "IMPORTANT"], "has_attachments": true, "size_bytes": 52480, "body": "<plain text body, or HTML when no plain text body exists>", "body_html": "<html><body><p>Full HTML body</p></body></html>", "attachments": [ { "filename": "q4-plan.pdf", "mime_type": "application/pdf", "size_bytes": 204800 } ]}