HTTP API reference

Languages and error codes

English is the default. The API, Web console and notification renderers share the embedded catalogs in internal/i18n/locales/. internal/i18n/languages.json defines these canonical codes and native names:

Code Language
en English
zh-CN 简体中文
zh-Hant 繁體中文
ja 日本語
ko 한국어
de Deutsch
fr Français
es Español
pt-BR Português (Brasil)
  • On first visit, the console uses the browser’s Accept-Language. Its selector stores the chosen canonical code locally. Switching the console language leaves saved notification settings unchanged.
  • API requests, including App management requests, negotiate Accept-Language by quality weights. Responses include Content-Language and Vary: Accept-Language. Unsupported preferences are skipped; missing, invalid or entirely unsupported preferences fall back to en. A language with weight 0 is excluded from negotiation.
  • Regional variants map to their supported language: fr-CA → fr, es-MX → es, pt / pt-PT → pt-BR. Chinese script is respected: zh-Hans / zh-CN / zh-SG → zh-CN; zh-Hant / zh-TW / zh-HK / zh-MO → zh-Hant. An explicit script takes precedence over region; plain zh uses Simplified Chinese.
  • GET /locales/default.json serves the negotiated catalog; GET /locales/{canonical-code}.json serves that catalog explicitly. Unknown catalog paths return locale_not_supported.
  • Delivery settings language controls notifications generated by the outpost independently of the request or console language. It accepts the canonical codes and supported regional variants, persists the canonical code, and defaults to en when omitted or empty. An unsupported explicit value returns 400 config_language_invalid.
  • The push delivery service generates Native test notification text using the paired device’s language. The outpost notification language does not override that language. The push delivery service and App each support their own set of languages.
  • Standard folder roles and common folder names are localized in the console. Raw IMAP paths and folder_roles remain unchanged in APIs; optional folder_names maps full paths to leaf names using the server’s hierarchy delimiter. The console displays the name with its full path and submits the original path. Owner mailbox responses include saved folder_display (names and roles), while App mailbox responses omit it to preserve folder permissions. Apps localize these roles with their own catalogs. Notification titles continue to use the original leaf folder name. User-entered names, email subjects, message bodies and verification codes remain literal.
  • Logs and CLI output use English. Business errors retain stable English snake_case codes and named parameters; storage keeps codes and parameters, and display layers translate messages. Dates and numbers in API responses keep their existing machine-readable formats; the console formats timestamps using its chosen locale.
json
{
  "error": {
    "code": "unauthorized",
    "message": "Sign in with a valid session or API token."
  }
}

last_error in delivery records and notice in folder status use the same {code, message, params?} shape, for example bark_http_error with {"status":"503"}. Branch on code; show message.

Each mailbox reserves one connection for its folder discovery and connection tests. Watcher demand is realtime Folder count + (1 if any scheduled Folders exist, otherwise 0), at most connection_limit - 1 connections (9 by default); all operations for that mailbox together stay within connection_limit (2–100). Saved configurations with a limit of 1 start in recovery mode with configuration_invalid.

connections_in_use is the instantaneous count of occupied connection slots. Scheduled checks close their shared connection between rounds. The console mailbox list instead shows configured monitoring demand / total limit (for example, 1 / 10); mailbox details show the same demand / monitoring capacity (1 / 9), reserving one slot for management.

HTTP API

GET / serves the current page; static assets and /healthz are public. Before setup, every other management endpoint returns 403 setup_required. POST /api/v1/setup is available once; POST /api/v1/session is public after setup. All remaining APIs require the session cookie or Authorization: Bearer mwk_….

Cookie sessions use HttpOnly, SameSite=Strict, Path=/, Secure on a TLS connection or an HTTPS Origin, a seven-day idle timeout and a 30-day absolute timeout. Every cookie-authenticated mutation requires X-CSRF-Token and an HTTP(S) Origin whose host and optional port exactly match the request Host. Bearer requests skip CSRF checks. An invalid Authorization header fails even when a valid cookie is present.

Login and setup code failures share a source-address budget: ten failures per 15 minutes, followed by 429 too_many_attempts with Retry-After in seconds. The source is Gin ClientIP using the trusted proxy ranges above. Password changes also use this budget. Explicit cross-site Origins are rejected on login and setup. Because every private-network peer is trusted as a proxy, a client on your local network can set X-Forwarded-For and spread its attempts across many source addresses. Do not expose the outpost listener to an untrusted local network; publish it through your reverse proxy only.

All JSON bodies are limited to 64 KiB; unknown fields or trailing JSON return 400. Passwords are 12 characters minimum and 1024 UTF-8 bytes maximum. Usernames and token names are 1–128 bytes. All management API timestamps are RFC 3339 strings in UTC, including delivery created_at/accepted_at, token created_at/last_used_at, folder checks, log times and diagnostic generation time. Fractional seconds preserve stored precision. Unused token last_used_at is null; deliveries without acceptance omit accepted_at. Counts, durations and revisions remain numbers.

POST /api/v1/session returns 401 login_credentials_invalid for an incorrect username or password, using the same message for both. Protected requests with invalid or missing authentication return 401 unauthorized.

An incorrect setup code returns 400 setup_code_invalid; the wizard returns to the code step and preserves the username. An incorrect current password on PUT /api/v1/admin/password returns 400 current_password_invalid and counts toward the failure limit; the current session remains valid. Login failures and current-password errors appear within the form, preserving its inputs.

Method Path Request / response
POST /api/v1/setup { "code":"XXXX-XXXX-XXXX", "username":"admin", "password":"…" }; 201, cookie + { "csrf_token":"…" }; repeat setup: 409
POST /api/v1/session { "username":"admin", "password":"…" }; 200, cookie + CSRF token
GET /api/v1/session { "authenticated":true, "csrf_token":"…" }; Bearer returns method: "token" instead of CSRF
DELETE /api/v1/session 204; revokes the current cookie session; requires a session
PUT /api/v1/admin/password { "current_password":"…", "password":"…" }; 204, revokes other sessions; Bearer revokes all sessions
POST /api/v1/tokens { "name":"automation" }; 201, { "id":"…", "name":"…", "created_at":"2026-09-29T01:02:03Z", "last_used_at":null, "token":"mwk_…" }
GET /api/v1/tokens { "tokens":[…] }; metadata only
DELETE /api/v1/tokens/:id 204; revokes a token; repeat deletion also returns 204
GET /api/v1/mailboxes { "mailboxes":[…] }; redacted mailbox views
POST /api/v1/mailboxes Create with the mailbox form below; 201, redacted view with generated id and revision 1
GET / PUT / DELETE /api/v1/mailboxes/:id Read / update with current revision / delete (204); missing ID returns 404 mailbox_not_found
POST /api/v1/mailboxes/test Test an unsaved form; returns { "folders":[…] }, keeps configuration unchanged
POST /api/v1/mailboxes/:id/test Test the form; reuse omitted credentials only for the same connection
GET /api/v1/mailboxes/:id/folders Discover selectable folders using this mailbox’s budget
GET / PUT /api/v1/mailboxes/:id/subscriptions Read / save subscriptions using their own revision; successful save returns 202
GET / PUT /api/v1/settings/delivery Read / save global delivery settings with their own revision
DELETE /api/v1/settings/delivery Clear saved channel and credentials with {revision}; 204, stale revision 409
POST /api/v1/settings/delivery/test Test the delivery form with saved credentials merged; 204, configuration unchanged
GET /api/v1/status Folder states, channel, delivery counts and scoped notices
GET /api/v1/diagnostics Download a redacted JSON report without runtime logs
GET /api/v1/logs Authenticated operational logs; after, level, mailbox_id, limit filters (see below)
GET /api/v1/deliveries Last 50 delivery records, including subject and mailbox/folder summary
POST /api/v1/test-push Send a test using saved settings; 204 when accepted, configuration unchanged
POST /api/v1/deliveries/:id/retry Requeue a dead task; 202

Delivery records include an optional message object: mailbox_id, mailbox_label, folder, subject (string or null) and test (boolean). Names are snapshots from enqueue time and survive mailbox renaming or deletion. A null subject means it was not retained; an empty string means the mail had no subject. Before notification settings are first saved, the default reminder-only policy omits titles. Enabling subject previews later preserves the unavailable titles of older records. Test notifications have test=true and no mailbox source. The API omits sender and body from this summary.

Mailbox creation example:

json
{
  "label": "Work mailbox",
  "host": "imap.example.org",
  "port": 993,
  "username": "user@example.org",
  "password": "…",
  "connection_limit": 10
}

The outpost allows at most 20 saved mailboxes, including invalid configurations awaiting repair; the next creation returns 400 mailbox_limit_exceeded. IDs are generated as mbx_ plus a random string and stay stable. label defaults to the username (email address), allows at most 64 Unicode characters and rejects line breaks. A username longer than 64 characters needs an explicit shorter label. Omitted labels on update keep the saved value; an empty label uses the submitted username.

Mailbox PUT uses the same form plus "revision":1 (the value read from GET), and requires host, port and username. Omitted password keeps the saved password when host, port and username stay unchanged; an explicit empty password is rejected. Omitted connection_limit keeps the current value; new mailboxes default to 10, range 2–100. POST creation omits revision or sends 0. Test routes accept the same fields without requiring a revision and keep the configuration unchanged. The unsaved test requires credentials; the saved-mailbox test merges omitted credentials only from the specified ID.

A mailbox GET view includes id, label, revision, host, port, username, connection_limit, connections_in_use and "password":{"configured":true}. A recovered invalid mailbox reports configured:false and keeps its ID and revision so it can be repaired. The GET password object is a view, not a valid write value.

Each mailbox has an independent connection budget. Realtime Folders each consume one slot; all its scheduled Folders together consume one. Their combined demand must fit within connection_limit - 1, reserving one slot for management. Subscription PUT and mailbox limit changes enforce this formula; exhausted capacity returns connection_budget_exceeded. Testing an unsaved mailbox uses a temporary budget. The unsaved test, saved-mailbox test and folder discovery endpoints additionally share a global concurrency limit of 2; excess requests return 429 discovery_busy immediately. Completion or cancellation releases admission.

Delivery PUT example for an initially unconfigured channel:

json
{
  "revision": 0,
  "channel": "bark",
  "preview": "off",
  "retry_count": 0,
  "language": "en",
  "bark": { "endpoint": "https://api.day.app", "key": "…" }
}

Read /settings/delivery before each update and send its revision; the first save advances 0 to 1. Pushover uses "pushover":{"token":"…","user":"…"}; Webhook uses "webhook":{"url":"https://hooks.example.org/mail","secret":"…"}. Channel, preview and revision are required. Omitted language and retry count use defaults. Omitted credentials, including Webhook URL, keep saved values; changing the Bark endpoint requires an explicit device key. An explicit empty active credential is rejected. GET replaces every credential with {"configured":true|false}. The separate test endpoint does not require revision.

Mailbox creates and connection changes test connection/login before saving. If host, port, username and connection_limit match the saved values and password is omitted, PUT changes only the label: revision checking and persistence still apply, while tests and watcher replacement are skipped. Queued tasks retain their original label; future tasks use the new label. Creating or updating an identity already used by another mailbox returns 409 mailbox_duplicate (host and username compare case-insensitively, port exactly); concurrent creation can save that identity only once. Delivery PUT saves all channels after field, address, selected pairing state and revision validation, without sending a test or contacting a provider. Explicit delivery tests leave the saved settings unchanged; their failure does not block a later save. Mailbox tests run outside configuration locks and keep existing watchers active; a failed mailbox test preserves its previous configuration. Stale settings revisions return 409 settings_conflict; a subscription change during a mailbox test also invalidates that test’s snapshot. Reload before retrying. Each mailbox and the global delivery settings have independent revisions, so edits to different mailboxes or to a mailbox and delivery do not conflict.

A successful PUT returns the redacted view and applies immediately. A mailbox connection change stops its own watchers before the save and optional cursor reset; a failed save restores them. Other mailboxes continue running. Delivery changes preserve the snapshot of an in-flight attempt; subsequent attempts use the new one. Pending tasks wait while delivery is unconfigured.

GET /api/v1/mailboxes/:id/subscriptions returns {"revision":1,"folders":[{"name":"INBOX","check":"realtime"},{"name":"Clients","check":"5m"}]}. PUT sends the revision read and the complete folders array; an empty array unsubscribes from everything. Names are unique within each mailbox, INBOX is normalized, and each name is at most 4096 UTF-8 bytes. Every Folder requires name and check; check is realtime, 5m or 15m. Missing/invalid checks and the previous string-array format return 400. The connection-demand formula above replaces the Folder count limit. Changing only check preserves progress, catches up new mail and keeps existing event IDs; cancelling and later subscribing again builds a fresh baseline. All fields are required. Saving returns 202 with the next revision; stale revisions return 409 subscriptions_conflict. Folder existence is confirmed by the watcher, so subscriptions can be removed while the mailbox is offline. Storage failures return 503 and preserve current subscriptions.

Every /status.folders[] item includes mailbox_id, mailbox_label and check. mode is idle, poll or scheduled; scheduled Folders also include next_check (RFC 3339 time, including retry times). Realtime Folders omit next_check. Saved configuration that fails JSON or model validation becomes unconfigured, with configuration_invalid in /status.notices under mailbox:<id> or delivery; PUT repairs that item. Excess subscriptions pause only that mailbox and report connection_budget_exceeded under subscriptions:<id>. Reducing subscriptions or increasing its limit resumes monitoring. Decryption failures stop startup.

The diagnostics allowlist contains generation time, build/Go/platform information, queue counts, and anonymous mailbox and watcher states, including check and mode. mailboxes[].index and watches[].mailbox_index identify “Mailbox 1”, “Mailbox 2”, and so on within that report; each mailbox’s watchers have their own index. Mailbox entries include configuration/subscription notice codes. Only imap_folder_rejected can include watches[].response_code. The report omits mailbox IDs, display names, addresses, server names, folder names, message data, task IDs, credentials and all other error parameters.

Task states: pending waits for a request, accepted means the channel accepted the request, dead means the attempt ended with a recorded failure.

Runtime logs

Use docker compose logs -f core (or docker logs <container>) for JSON stderr output. Authenticated clients can query the same operational events through GET /api/v1/logs?after=0&level=info&mailbox_id=mbx_example&limit=200. A session cookie or API token is required.

The in-memory ring keeps the newest 2000 Info/Warn/Error entries and clears on restart. after is an exclusive non-negative sequence; level selects that level and above (info default); omitted mailbox_id includes all mailboxes and global operations. limit defaults to 200, range 1–500. Invalid parameters return 400 logs_request_invalid.

json
{
  "entries": [
    {
      "seq": 42,
      "time": "2026-09-29T00:00:00Z",
      "level": "info",
      "message": "Folder connection established",
      "attrs": {
        "mailbox_id": "mbx_example",
        "folder": "INBOX",
        "mode": "idle",
        "check": "realtime"
      }
    }
  ],
  "next": 42,
  "cleared_through": 0
}

Use next as the next request’s after. At the page limit it is the last returned sequence; after scanning the snapshot it advances to the snapshot tail, including when filters match nothing. Evicted entries are skipped. If restart returns next < after, reset the cursor to 0. Entries are ordered by sequence.

Logs cover mailbox and Folder lifecycles, baseline/enqueue counts, delivery attempts, configuration changes and administrator actions. Global events use an empty mailbox_id. Only these scalar attributes are exposed: mailbox_id, folder, mode, check, code, event_id, channel, attempt, duration_ms, http_status, next_attempt, backoff_seconds, count, revision, token_id, version, listen, request_id, method, route, stage, outcome, cause_code, response_code. http_status=0 means no HTTP response was received; token_id is a public record identifier, not the token value. Passwords, token/session values, setup codes, sender, subject, bodies and credential URLs are omitted. The setup-code line goes only to the console. Logs contain Folder names; the anonymous diagnostic export excludes logs and names.

Folder discovery and administrator mailbox tests return folders as the original path array plus optional folder_roles, a map from path to inbox, drafts, sent, trash, junk, archive, all or flagged. Roles come from IMAP SPECIAL-USE attributes; INBOX is recognized by its protocol-defined name. The Web UI falls back to case-insensitive exact matches for common English folder names when roles are absent. Translated names show the original path in parentheses; other names remain unchanged. Display translations never change subscription paths. Notification titles use the final segment of slash-separated folder paths; structured folder fields retain the complete path. App mailbox tests continue returning only connection status.

GET /api/v1/session includes username for an authenticated cookie session. Token authentication keeps its existing response.

App receiving device

Delivery settings include native_pairing_id. For the native channel, select one active pairing from /api/v1/native/devices. Both test notifications and new events target that pairing only. Omitting the field on an update preserves the saved selection; an empty selection returns native_target_required. An unavailable selection returns native_target_unavailable. Revoking the selected native pairing atomically clears channel and native_pairing_id and increments revision once. Other targets and channels remain intact. Existing deliveries retain their target and revocation result. See pairing synchronization for timing, concurrency and restart behavior.

App authorization manages administration and push permission independently. Revoking either permission leaves the other unchanged.

Native test notifications

The outpost uses signed POST /v1/pairing-tests with {pairing_id,test_id} (a fresh UUID per explicit test). The push delivery service generates fixed content and synchronously submits it to APNs without requiring Native Push entitlement. Form tests, App saved-setting tests and /api/v1/test-push use this same path. Tests are separate from the mail Outbox and have no automatic delivery retries.

The outpost returns HTTP 204 only when the push delivery service reports accepted. APNs rejection, unknown outcomes and sending return a localized error; the push delivery service rate limiting makes the outpost return HTTP 429 with Retry-After. Safe logs include the push delivery service HTTP status and stable error code, excluding response bodies, tokens and notification contents. The outpost and App share the push delivery service’s existing per-device minute and per-subject daily quotas and (device_id,test_id) idempotency. A user starting another test creates a new test ID.

Formal mail uses /v1/push and remains entitlement-gated regardless of event ID or client test flags. Deploy the push delivery service pairing-test route before this outpost version; the outpost does not fall back to paid mail delivery.

Clear delivery history and runtime logs

  • DELETE /api/v1/deliveries returns 200 {"deleted":N}. It permanently removes finished records (accepted / dead) across the full history, including their associated Native records. Pending deliveries/retries, Native records still pending/queued/sending or scheduled for status checks, and pending Native activity updates are retained. Clearing failures removes their manual retry option. Mailbox checkpoints and configuration are preserved.
  • DELETE /api/v1/logs returns 200 {"cleared_through":N} and clears the shared in-memory ring for all levels and mailboxes. Sequence numbers continue increasing; stderr/container logs and exported files remain. GET /api/v1/logs also returns cleared_through; clients discard cached entries with seq <= cleared_through before appending the returned entries.
  • Both endpoints require administrator session + Origin/CSRF, or an administrator API token. App device credentials do not grant these operations. Repeated clearing is safe; new work and log entries may appear after the operation.

App message content

POST /api/v1/app/content uses a device-bound controller credential and owner content scope. Send { "reference": <message_ref> }; the response includes text, truncated, optional sanitized html, and has_remote_images. Reading is free.

After the body becomes readable, POST /api/v1/app/content/read with the same credential, scope and reference marks the message as read in the mailbox. Success returns 204 No Content with Cache-Control: no-store. Fetching content alone preserves the mailbox read state. Both operations verify the current authorization, subscribed folder, mailbox identity and message reference.

Changing credential targets

For mailbox tests and saves, changing host, port or username requires re-entering a saved mailbox password. Missing, null, empty or invalid replacements return 400 mailbox_password_required_for_change. Changing the Bark service address requires re-entering a saved device key, including when another channel is selected; otherwise the request returns 400 bark_key_required_for_change. Address changes include port, path and trailing-slash changes. Omitting the Bark address keeps its saved value.

Rejected requests open no connection, send no notification and preserve configuration and revision. Same-target edits retain omitted credentials. A test with explicitly supplied credentials keeps the saved configuration unchanged. Read responses continue to show only whether credentials are configured.

App request diagnostics

Diagnostic exports use schema_version: 2 and include app_requests: up to 300 recent completed App requests from the shared 2000-event memory log. Each entry contains the time, server-generated request_id (also returned in X-Request-ID), route template, method, HTTP status, duration, stage, outcome and safe error codes. Resource IDs, query strings, credentials, signed references, message bodies and full runtime logs are excluded. Restarting the outpost or clearing logs removes these entries; other log events can evict older requests. Download refreshes the report first. Requests that never reach the outpost require App diagnostics.

For POST /api/v1/app/content/read, the outpost queries UID and flags. An already-read message succeeds; an unread message is marked and checked again. STORE OK alone does not prove the message exists. Missing UID, changed UIDVALIDITY or an explicitly nonexistent folder returns 404; provider or permission failures return 503, and timeout returns 504. Cached bodies remain readable when their remote reference expires.

Markdown · AGPL-3.0-only · Mailwake contributors