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-Languageby quality weights. Responses includeContent-LanguageandVary: Accept-Language. Unsupported preferences are skipped; missing, invalid or entirely unsupported preferences fall back toen. 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; plainzhuses Simplified Chinese. -
GET /locales/default.jsonserves the negotiated catalog;GET /locales/{canonical-code}.jsonserves that catalog explicitly. Unknown catalog paths returnlocale_not_supported. -
Delivery settings
languagecontrols 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 toenwhen omitted or empty. An unsupported explicit value returns 400config_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_rolesremain unchanged in APIs; optionalfolder_namesmaps 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 savedfolder_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_casecodes 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.
{
"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:
{
"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:
{
"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.
{
"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/deliveriesreturns200 {"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/logsreturns200 {"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/logsalso returnscleared_through; clients discard cached entries withseq <= cleared_throughbefore 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