Repository navigation
The REST API
This page began as a chapter of the 6.2.10 manual and has been corrected for 6.2.24, then checked and extended against the 6.2.28 source on 8 September 2026. The Control Panel's pages are grouped differently now, so the paths below use today's groups; the TLS ports 465, 993 and 995 exist only after you create them on the TCP/IP ports page (a fresh install seeds 25, 587, 110 and 143); and everything added since 6.2.10 is in Changes-Since-6210. Where a value here disagrees with the Settings Reference, which is generated from the code, the reference is right. What 6.3.4 added - the credentials, the sign-in methods and the route families - was read again against master
540347500on 24 September 2026, and against92b8f9f63on 26 September 2026, before the last batch the release, 6.3.4 (27 September 2026, schema 6104), carries: the route families of eDiscovery, mail in to a webhook, mailbox encryption at rest, rooms and equipment, the message store's measurement and the webmail's select-all, and the counts in section 5. Brought up on 5 October 2026 to 6.3.6 (6 October 2026, schema 6108), for what 6.3.6 changes on this page. 6.3.7, the latest release (17 October 2026, schema 6108), is not yet reflected here.
A modern alternative to COM for automation, and the only option from a non-Windows
machine. Everything on this page was read out of Server/Common/Util/RestApiServer.cpp
and Server/Common/Util/HttpServer.cpp; the exhaustive route-by-route reference is
APIs Reference section 4. Two command-line clients speak this API rather than COM - hmctl for any shell and a PowerShell module - and they are on Command Line.
[Settings]
RestApiPort=8443
RestApiBindAddress=127.0.0.1Restart the service (or call COM Application.Reinitialize). Both keys are read once,
when the listener is constructed at server start, so editing them without a restart
changes nothing. Their editors are on Monitoring & troubleshooting → Monitoring & alerts, on its REST API tab.
Since 6.3.4, the file edit above works only once. From schema 6042 the
[Settings]keys are stored in the database andhMailServer.INIis a copy. A key the server has never stored is taken in from the file at the next start — so on a fresh install, editing the file before the first start works. A key it has stored is not taken from the file: at the next start the stored value is used, the edit is named in the error log as HM5804, and the line is put back to the stored value where the service may write the file — underNT SERVICE\hMailServer, and under the Linux packages'hmailserveruser, to either of which the file is read-only, the edit stays in the file (The Service Account). Change a storedRestApiPorton the Control Panel page above, over COM withSettings.SetIniSetting, or — where neither is available — in a[SettingsOverride]section of the same file, which is applied over the store at every start and reported as HM5807 each time.This matters most on Linux, where the REST API is the administration interface. The packaged
/etc/hmailserver/hMailServer.inishipsRestApiPort=0andRestApiBindAddress=127.0.0.1under[Settings], so the first start stores both; after that, changing the port in[Settings]turns nothing on. Put the lines under[SettingsOverride]instead:[SettingsOverride] RestApiPort=8080The container image's
HM_REST_PORTand related variables write[Settings]too, so on a container whose database already holds the key, the variable is undone the same way.

The listener does not always start. It refuses in three named ways, and each one says so once in the application log:
| What is wrong | Log line | What you see from a client | Fix |
|---|---|---|---|
[Security] AdministratorPassword is empty |
RestApi: Refusing to start - the administrator password is not set. |
Connection refused; nothing is listening | Set an administrator password (Control Panel, or COM Settings.SetAdministratorPassword) |
RestApiBindAddress is not loopback and no certificate is configured |
RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile. |
Connection refused | Set RestApiCertificateFile + RestApiPrivateKeyFile, or enable ACME, or bind loopback |
| The configured certificate cannot be loaded, or its key does not match |
RestApi: Refusing to start - the shared TLS configuration could not be applied to the configured certificate. (preceded by HM5113 naming the file) |
Connection refused | Fix the PEM paths and permissions |
| Nothing is wrong |
RestApi: Listening on 127.0.0.1:8443 (http, loopback only). or ... (https).
|
It answers | — |
The exact loopback literals that exempt the listener from TLS are 127.0.0.1,
localhost and ::1. Not 127.0.0.2, not ::ffff:127.0.0.1 — the check is a string
comparison against those three, deliberately, because it is a security gate.
flowchart TD
A["Service start, RestApiPort read"] --> B{"Is RestApiPort greater than zero?"}
B -- no --> Z["No listener. This is the shipped default."]
B -- yes --> C{"Is AdministratorPassword set?"}
C -- no --> R1["Refuse: the administrator password is not set"]
C -- yes --> D{"RestApiCertificateFile and RestApiPrivateKeyFile both set?"}
D -- yes --> G["HTTPS"]
D -- no --> E{"Do ACME fullchain.pem and privkey.pem both exist?"}
E -- yes --> F["Use the ACME certificate. Logs: RestApi: Using the ACME certificate for HTTPS."]
F --> G
E -- no --> H{"Is the bind address 127.0.0.1, localhost or ::1?"}
H -- yes --> I["Plain HTTP, loopback only"]
H -- no --> R2["Refuse: TLS certificate is required"]
G --> J["TLS context from SslContextInitializer, then a TLS 1.2 floor"]
J --> K["Listening"]
I --> K
Source: Application.cpp (the ACME substitution and the start order), RestApiServer::Start.
TLS configuration is shared with the mail protocols. The context is built by
SslContextInitializer::InitServer, so the cipher list, the protocol toggles, the DH
parameters and TlsKeyExchangeGroups (including the hybrid post-quantum groups) apply
here exactly as they do to SMTP and IMAP. One thing is deliberately not shared: a
TLS 1.2 minimum is applied on top, so opening TLS 1.0 up for an ancient mail client does
not open it up for the management API.
Three, and they do not overlap.
| Credential | Header | Reaches | Restrictable | Where it comes from |
|---|---|---|---|---|
| Administrator password |
Authorization: Basic base64("Administrator:<password>") (user name case-insensitive) |
Everything except /api/v1/me* and /api/v1/session
|
No — full authority, by design | [Security] AdministratorPassword |
| API key | Authorization: Bearer hmapi_<64 hex> |
Everything except key management, /api/v1/settings/ini*, /api/v1/me* and /api/v1/session — and, since 6.3.4, /api/v1/audit*, /api/v1/alerts*, /api/v1/webhooks*, the compromised-account review, /api/v1/settings/changed, /api/v1/server/settings/pending, an account's export, the address erasure and the second factor's enrolment — each refused with 401 |
Yes — scope, domains, source addresses, expiry |
Monitoring & troubleshooting → REST API keys, or POST /api/v1/apikeys
|
| An account's own |
Authorization: Basic base64("<address>:<password>"), or the hmailsession cookie |
/api/v1/me* and /api/v1/session only |
n/a | The mailbox password, or one of its app passwords |
Since 6.3.4, the three overlap in one place and gain two uses. An account that holds an administrator role (schema 6047: serveradmin, domainadmin, helpdesk, auditor or a custom one) reaches the administrative routes its role's permissions allow, in the domains it reaches, with its own address and password; a route its role may not call answers 403 naming the role and the permission it lacks. An API key holds a role too, and may name a sender (send_from, with ceilings a minute and a day), which is the only credential POST /api/v1/send takes; a key whose role holds provision is the only credential /scim/v2 takes. And a one-time recovery code, in X-hMailServer-Recovery-Code, is taken wherever the authenticator's code is asked for. Roles.md, SendApi.md, Scim.md.
The two Basic forms are told apart before either password is tried: any user name but
administrator is treated as a mailbox address and goes down AccountLogon::Logon, the
same path an IMAP logon takes. So an account is never tested against the administrator
password, and the administrator's name is never tested against the accounts.
When a second factor is enrolled on the administrator credential
([Security] AdministratorTotpSecret), the code goes in an extra header:
curl -u Administrator:your-admin-password \
-H "X-hMailServer-OTP: 123456" \
http://127.0.0.1:8443/api/v1/statusOmit it and you get a 401 whose body and headers say what is missing — the only 401 on this listener that distinguishes itself from any other:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="hMailServer"
X-hMailServer-OTP: required
Cache-Control: no-store
{"error":"authentication failed","second_factor":"required"}A missing code is not counted towards the auto-ban (the password was right; the client simply had not been told). A wrong code is.
Read-only by default, expiring by default (90 days; there is no "never expires"), and
optionally narrowed to named domains and to a source address, range or CIDR. Only the
SHA-256 of a key is stored, in hMailServerApiKeys.ini beside hMailServer.ini, and the
file is re-read on every authentication attempt — so deleting a section revokes a key
on the next request with no restart.
# Mint one. Administrator password only: a key can never mint or revoke keys.
curl -u Administrator:your-admin-password \
-H "Content-Type: application/json" \
-d '{"label":"backup monitor","scope":"readonly","expires":"2027-01-01 00:00:00"}' \
http://127.0.0.1:8443/api/v1/apikeys{"id":"3f9a1c4b5d6e7f80","label":"backup monitor","scope":"readonly","domains":"",
"expires":"2027-01-01 00:00:00","allowed_from":"","key":"hmapi_7c1f...c92a"}key is the clear text, returned once and never recoverable. Store it now.
curl -H "Authorization: Bearer hmapi_7c1f...c92a" http://127.0.0.1:8443/api/v1/statussequenceDiagram
autonumber
participant C as Client
participant S as HttpServer accept loop
participant N as HttpConnection
participant R as ProcessRequest_
participant A as Authorize_
participant H as Handler
C->>S: TCP connect
S->>S: accept_filter_ - is this address in the refused set?
alt refused, or 64 connections already open
S-->>C: connection closed, no response
else accepted
S->>N: Start, arm the 300 s connection timer
opt TLS configured
N->>C: TLS handshake, inside the 30 s request deadline
end
C->>N: request line and headers
N->>N: cap head at 64 KB, reject NUL, parse Content-Length
opt POST to /api/v1/me/messages or /api/v1/me/drafts
N->>N: raise the cap to 16 MB and the deadline to 300 s
end
N->>C: 100 Continue, only if the client asked and the length passed the cap
C->>N: body
N->>R: HttpRequest with raw head plus body
R->>R: Authenticate_ - Bearer, then Basic, then the session cookie
alt no credential matched
R-->>C: 401 authentication failed
else authenticated
R->>R: CSRF check for cookie-authenticated writes
R->>R: rate budget, 200 requests per 10 s for this credential
R->>R: ParseRoute_ turns method plus path into a RouteKind
R->>A: Authorize_ with the caller and the route
alt allowed
A-->>R: allowed
R->>H: run the handler
H-->>R: HttpResponse
else forbidden
A-->>R: 403 with the reason
else key touched /api/v1/apikeys
A-->>R: 401, never 403
end
end
R-->>N: status, JSON body, Cache-Control no-store
N-->>C: response, connection kept alive unless it was a refusal
end
Sources: HttpServer::Accept_, HttpConnection::OnHead_ / OnBody_,
RestApiServer::ProcessRequest_, RestApiServer::Authenticate_, RestApiServer::Authorize_.
Two things in that diagram are worth pulling out.
Authorisation happens in exactly one place. ParseRoute_ turns the method and path
into a RouteKind; Authorize_ decides on the kind; only then is a handler called. A
handler cannot be reached by a credential nobody checked, and a new endpoint cannot be
added without appearing in Authorize_.
Read-only is decided by route kind, not by HTTP method. A route that changed
something under a GET could not slip past a read-only key by being spelled harmlessly.
flowchart TD
A["Authorize_ decides on the caller and the route"] --> B{"Is this a self-service route?"}
B -- yes --> C{"Is the caller an account?"}
C -- yes --> OK1["Allowed"]
C -- no --> F1["403 - this endpoint answers to an accounts own credentials"]
B -- no --> D{"Is the caller an account?"}
D -- yes --> F2["403 - an accounts credentials reach only /api/v1/me"]
D -- no --> E{"Is the caller the administrator?"}
E -- yes --> OK2["Allowed - full authority, nothing narrows it"]
E -- no --> G{"Is it key management, settings/ini, or - since 6.3.4 - audit or alerts?"}
G -- yes --> U["401 - a key learns nothing about these, not even that they exist"]
G -- no --> H{"Read-only key on a mutating route?"}
H -- yes --> F3["403 - this api key is read-only"]
H -- no --> I{"Does the key have a Domains list?"}
I -- no --> OK3["Allowed"]
I -- yes --> J{"Is it a queue or quarantine route, or the volume or storage report?"}
J -- yes --> F4["403 - the queue / the quarantine / that report is server-wide"]
J -- no --> K{"IP ranges, certificates, rules, logs, backup, settings, update, groups or remote domains?"}
K -- yes --> F5["403 - that resource is server-wide"]
K -- no --> L{"Does the route name a domain or an address?"}
L -- no --> OK4["Allowed - status, tlsa, srv and the domain list filter in their handlers"]
L -- yes --> M{"Is that domain in the keys list?"}
M -- yes --> OK5["Allowed"]
M -- no --> F6["403 - this api key is not permitted for that domain"]
Source: RestApiServer::Authorize_, RestApiServer::IsMutatingRoute_,
RestApiServer::IsSelfServiceRoute_, RestApiServer::IsApiKeyRoute_. The report routes of
6.3.4 take one more branch the diagram folds into K and M: a
domain-restricted key that names no domain is not refused but confined to its own domains
by the handler, as the domain listing is.
New in 6.2.28. GET /portal serves a static sign-in page compiled into the server; its
script signs in and then talks to /api/v1/me. The page and its script are the only
unauthenticated routes besides the WebAdmin shell at /.
sequenceDiagram
autonumber
participant B as Browser
participant P as RestApiServer
participant L as AccountLogon
participant T as Session table in memory
B->>P: GET /portal
P-->>B: static HTML, CSP script-src 'self', no inline script
B->>P: GET /portal.js
P-->>B: static script
Note over B: the person types address and password
B->>P: POST /api/v1/session, Authorization Basic address:password
P->>L: Logon - the same path, lockout and auto-ban as IMAP
alt refused
L-->>P: no account
P-->>B: 401 authentication failed
else accepted
L-->>P: account
P->>T: drop expired sessions; a fifty-first of this holder's ends its least recently used; a full table takes from the largest holder (6.3.6)
P->>T: store SHA-256 of a fresh 32-byte token with the account id
P-->>B: 201 plus Set-Cookie hmailsession=... HttpOnly SameSite=Strict Max-Age=43200 [Secure]
end
Note over B: the password is not kept - the cookie is the credential from here on
B->>P: GET /api/v1/me, Cookie hmailsession=...
P->>T: constant-time compare, check 30 min idle and 12 h absolute
P->>P: re-read the account from the database on every request
P-->>B: 200 the account's own state
B->>P: PUT /api/v1/me/vacation, Cookie plus X-Requested-With hMailServer
P-->>B: 200
B->>P: DELETE /api/v1/session
P->>T: forget this session
P-->>B: 200 plus a clearing Set-Cookie
Source: RestApiServer::HandleSessionCreate_, AuthenticateSession_, HandleSessionDelete_,
AuthenticateAccount_.
The cookie's rules, all enforced server-side:
| Rule | Value | Why |
|---|---|---|
| Token | 32 random bytes as hex; only its SHA-256 is kept, in memory | The table leaks nothing if it is read |
| Idle ceiling | 30 minutes | |
| Absolute ceiling | 12 hours from sign-in | A captured cookie is useful for a bounded time, not for as long as the victim keeps clicking |
| Table size | 1,000 sessions, and since 6.3.6 at most 50 per account or the administrator; a full table takes from the largest holder, and the administrator's sessions do not count towards the thousand | A script cannot fill memory with sign-ins, and one person cannot sign others out (up to 6.3.5 a full table dropped its least recently used session whoever held it) |
| A session cannot start a session |
POST /api/v1/session with a cookie → 403 |
|
| Account re-read every request | Deactivated or deleted → refused and its sessions dropped | |
| Password change | Ends the account's other sessions | |
| Listener stop | Every session is dropped | |
| CSRF | A cookie-authenticated request that is not GET or HEAD must carry X-Requested-With: hMailServer
|
SameSite=Strict closes it once; a header no browser adds cross-origin closes it twice |
The CSRF refusal is verbatim:
{"error":"a request that changes something must carry X-Requested-With: hMailServer when it is authenticated by a session cookie"}Since 6.3.4, a session is minted four more ways, all through the same POST /api/v1/session and so under the same table, the same ceilings and the same device list:
-
A passkey (WebAuthn, schema 6077): the page asks
POST /api/v1/passkeys/challengefor a challenge - thirty-two random bytes, five minutes, spent on first use - and presents the browser's assertion. An account enrols one from its Security page, and the administrator from the Deck; enrolling asks for the password again, and the one-time code where one is enrolled. -
OpenID Connect from the one issuer the administrator names (
/portal/oidc/start, the authorization code with PKCE, theid_tokenverified against the issuer's keys) or SAML 2.0 from an identity provider that speaks only that (/portal/saml/start,/portal/saml/acs, the Response's signature verified over its exclusive canonical form): each ends in a one-use ticket presented where the password would be. Neither makes a mailbox unless auto-provisioning is on, and neither opens the Deck but for an address the administrator listed. IMAP, POP3 and SMTP still take the password. -
A second account in the same page:
POST /api/v1/session?bearer=1answers the session as a token in the body, which the page keeps in memory and sends asAuthorization: Bearer; it is never minted for the administrator. -
After a forgotten password:
POST /api/v1/password-resetalways answers 202 with one sentence, and a link to the confirmed recovery address opens/portal/reset.
Every session is also a row the account and the administrator can see and revoke (/api/v1/me/devices, /api/v1/accounts/<address>/devices, schema 6053), and the idle and lifetime ceilings, one session per device and binding to the address are a policy of the server, a domain or an account. PasskeysAndSingleSignOn.md, SecondAccount.md, AccountRecovery.md, DevicesAndSessions.md.
Full detail — request bodies, every error code, the domain and read-only rules per route — is in APIs Reference section 4. This is the map.
| Method | Endpoint | Credential | Does |
|---|---|---|---|
GET |
/api/v1/status |
admin, key | Server status and session counts |
GET / POST
|
/api/v1/domains |
admin, key; POST needs a full key that is not domain-restricted | List domains (a domain-restricted key sees only its own; each entry is name, active, postmaster) / create a domain from {name, active, postmaster}
|
PUT / DELETE
|
/api/v1/domains/<name> |
admin, full key; DELETE never a domain-restricted one | Switch a domain on or off and set its postmaster ({"active":bool, "postmaster":...}, active required) / delete it with its accounts, aliases, lists, domain aliases and directories |
GET / POST
|
/api/v1/domains/<name>/accounts |
admin, key; POST needs a full key | List / create accounts |
PUT / DELETE
|
/api/v1/accounts/<address> |
admin, full key (the account's domain) | Update any subset of active, password, max_size_mb, first_name, last_name, forwarding, signature and admin_level ("server", and a server administrator's account, only for a credential that is not domain-restricted) / delete an account |
GET / POST
|
/api/v1/domains/<name>/aliases |
admin, key; POST needs a full key | List / create a domain's aliases (name, value, active) |
DELETE |
/api/v1/aliases/<address> |
admin, full key (the alias's domain) | Delete an alias |
GET / POST
|
/api/v1/domains/<name>/lists |
admin, key; POST needs a full key | List / create distribution lists |
DELETE |
/api/v1/lists/<address> |
admin, full key | Delete a distribution list |
GET |
/api/v1/domains/<name>/dkim |
admin, key | A domain's DKIM signing configuration |
GET |
/api/v1/queue |
admin, key (never a domain-restricted one) | The delivery queue |
POST |
/api/v1/queue/<id>/retry |
admin, full key | Retry a queued message |
DELETE |
/api/v1/queue/<id> |
admin, full key | Delete a queued message |
GET |
/api/v1/quarantine |
admin, key (never domain-restricted) | Quarantined messages |
POST |
/api/v1/quarantine/<id>/release |
admin, full key | Release one to its recipients |
DELETE |
/api/v1/quarantine/<id> |
admin, full key | Delete one |
GET |
/api/v1/archive?... |
admin, key (must name one of its domains) | Search the archive index; /archive/<id> one entry; POST/DELETE /archive/<id>/hold legal hold |
GET / POST
|
/api/v1/ipranges |
admin, key (never domain-restricted); POST needs a full key | List / create IP ranges; DELETE /api/v1/ipranges/<id>
|
GET / POST
|
/api/v1/certificates |
admin, key (never domain-restricted); POST needs a full key | The SSL certificates (never a key password) / add one by name and file paths, with an optional write-only key password; a file that is not there is a 400 |
DELETE |
/api/v1/certificates/<id> |
admin, full key | Delete a certificate; 409 while a port binds it |
GET / POST
|
/api/v1/ports |
admin, key (never domain-restricted); POST needs a full key | The listeners: protocol, address, port, connection_security, certificate_id, and since 6.3.6 smtp_authentication (on, off, honeypot; SMTP listeners only) / add one |
PUT / DELETE
|
/api/v1/ports/<id> |
admin, full key | Replace / delete a listener (a PUT that leaves smtp_authentication out writes on, 6.3.6). Port changes take effect on a restart in place (/api/v1/server/reinitialize) or a service restart |
POST |
/api/v1/server/reinitialize |
admin, full key (never domain-restricted) | Stop every service, reload the configuration and start them again in the same process - what the Control Panel's Reinitialize does. Answers 202 before it happens; poll /api/v1/status until it answers again |
GET / POST
|
/api/v1/rules |
admin, key (never domain-restricted); POST needs a full key | The global rules with their criteria and actions / create one; a rule acts on the next message delivered |
PUT / DELETE
|
/api/v1/rules/<id> |
admin, full key | Replace the whole rule (criteria and actions in array order) / delete it |
GET / POST
|
/api/v1/routes |
admin, key (never domain-restricted); POST needs a full key | The SMTP routes with their address lists / create one |
PUT / DELETE
|
/api/v1/routes/<id> |
admin, full key | Replace the whole route (the relay password is kept when absent) / delete it |
GET |
/api/v1/logs, /api/v1/logs/<name>?lines=N
|
admin, key (never domain-restricted) | Log files and a log's tail |
GET / POST
|
/api/v1/backup |
admin, key / full key | Backup status / start a backup |
GET / PUT
|
/api/v1/settings, /api/v1/settings/antispam, /api/v1/settings/logging
|
admin, key (never domain-restricted); PUT needs a full key | Each group whole on GET; PUT any subset of its keys, validated as the Control Panel validates them and applied only when every key is accepted. The relay password is write-only; the OpenAPI document names the keys that take effect on restart. 6.3.6 adds auto_ban_widen_by_reverse_name to /settings, and refuse_helo_not_fqdn, refuse_mail_from_domain_not_fqdn and surbl_too_many_domains_score to /settings/antispam
|
GET |
/api/v1/tlsa |
admin, key | Publish-ready DANE TLSA records |
GET |
/api/v1/srv |
admin, key | Publish-ready client-discovery SRV records |
GET |
/api/v1/metrics/history?metric=&range=24h|7d|30d |
admin, key | One metric's history |
GET / POST / DELETE
|
/api/v1/apikeys[/<id>] |
administrator password only | Manage API keys |
GET |
/api/v1/openapi.json |
admin, key | The OpenAPI description |
GET / POST
|
/api/v1/groups |
admin, full key for POST (never domain-restricted) | The account groups a folder permission can name as one principal / create one. 6.3.4. |
GET / PUT / DELETE
|
/api/v1/groups/<id> |
admin, full key | Read, rename, delete a group. 6.3.4. |
GET / POST
|
/api/v1/groups/<id>/members |
admin, full key | The group's accounts / add one by address or id. DELETE .../members/<account> removes one. A membership is a pair, so it is its own collection rather than a property of either side. 6.3.4.
|
POST |
/api/v1/accounts/<address>/messages/<id>/copy |
admin, key scoped to the address's domain | Copy the message into another folder of the same account (folder_id in the body): the file is copied, the row takes the destination's next UID, every session on that folder is told, and the source is left as it is. 6.3.4.
|
POST |
/api/v1/rules/match |
admin, key (never domain-restricted) | Try one rule criterion (match_value, match_type, test_value) and answer what the delivery path would decide. Nothing is read or written - the probe the Control Panel's rule editor needs. 6.3.4.
|
GET / PUT / DELETE
|
/api/v1/settings/ini, /api/v1/settings/ini/<name>
|
administrator password only (a key of any scope gets 401) | The [Settings] keys one at a time. In 6.3.3 a PUT writes the file and a DELETE removes its line. Since 6.3.4 both go to the settings store in the database first and bring the file's copy into line where the service may write the file (under NT SERVICE\hMailServer, and under the Linux packages' hmailserver user, the row alone - The Service Account); a DELETE is now the only way to return a stored key to its default - where the service may not write the file and the file has the line, the line stays and the row is kept holding the key's default, or the removal is refused for PasswordPepper and a key this build does not read - because a line deleted by hand is written back where the service may write the file, and the row kept either way. In 6.3.3 the list answers key names only; since 6.3.4 it answers each with its value too, a secret's as (set)
|
POST |
/api/v1/calendar/expand |
admin, key (never domain-restricted) | Expand one iCalendar object's recurrence over a window without storing it - what CalDAV's calendar-query would compute. 6.3.4.
|
GET / POST
|
/api/v1/remote-domains |
admin, key (never domain-restricted); POST needs a full key | The remote domain policies / create one: outbound and inbound TLS, size and rate limits, replies and forwards, the recipient callout (Routes and Relays §14.2a). 6.3.4. |
PUT / DELETE
|
/api/v1/remote-domains/<id> |
admin, full key | Replace the whole policy (a field left out takes the create's default) / delete it. 6.3.4. |
GET |
/api/v1/remote-domains/effective?domain=<name> |
admin, key (never domain-restricted) | The record that governs a delivery to that domain - exact over pattern, longer pattern over shorter, inactive skipped; 404 when none does. 6.3.4. |
POST |
/api/v1/remote-domains/verification-cache/clear |
admin, full key (never domain-restricted) | Forget every remembered callout verdict; answers how many. 6.3.4. |
GET |
/api/v1/reports, /api/v1/reports/<section>
|
admin, key - read-only is enough; a domain-restricted key sees its own domains and is refused volume and storage
|
What happened, per domain and per day: traffic, failures, senders, recipients, mailboxes, volume, storage, summary; from, to, domain, top, format=csv (Monitoring and Health §16.6a). 6.3.4.
|
GET |
/api/v1/audit, /api/v1/audit/verify
|
administrator password only | Who changed what, with filters and paging / walk the hash chain (Monitoring and Health §16.7b). 6.3.4. |
GET / PUT
|
/api/v1/alerts/rules, /api/v1/alerts/rules/<condition>
|
administrator password only | The alert rules / change or create one; the webhook secret is write-only. 6.3.4. |
GET / POST
|
/api/v1/alerts/events, /api/v1/alerts/test, /api/v1/alerts/run
|
administrator password only | What has fired / raise a condition on request / run one alerting pass now (Monitoring and Health §16.7a). 6.3.4. |
GET |
/deck-lang/<code>.json |
none | One language's catalogue for the Control Deck, read from WebAdmin/languages/<code>.json; 404 for a code that is not a file there. 6.3.4.
|
GET / POST / PUT / DELETE
|
/api/v1/me/calendars[/<id>[/shares/<address>]], /api/v1/me/calendar/events[/<id>[/reply]], /api/v1/me/tasks[/<id>], GET /api/v1/me/freebusy
|
account | The webmail's calendars and tasks: several calendars with colours, shared with an account or a group as reader, writer or delegate; a window's events occurrence by occurrence; an invitation answered as the owner or a delegate; free-busy for the caller's own domain. 6.3.4. |
GET / POST
|
/api/v1/me/messages/<id>/invitation, .../invitation/reply
|
account | The invitation a message carries, and the answer mailed to the organiser (iMIP). 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/me/contacts[/<id>], /api/v1/me/contacts/<id>/photo, /api/v1/me/contacts/groups[/<id>[/members[/<ref>]]], /api/v1/me/address-books[/<id>[/shares[/<target>]]]
|
account | The address book (6.3.3), with collected addresses as suggestions until kept, a card's photo, contact groups and several address books shared with an account or a domain since 6.3.4. |
GET / PUT
|
/api/v1/me/directory, /api/v1/me/directory/me[/photo], /api/v1/me/directory/<id>/photo, /api/v1/me/directory/<address>/smime
|
account | The organisation's directory of the reader's own domain, the reader's own entry, and a colleague's certificates from the domain's S/MIME authority. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/me/fetch-accounts[/<id>[/download]], /api/v1/me/fetch-accounts/import, /api/v1/me/sender-addresses[/<id>[/verify|/resend]], /api/v1/me/masked-addresses[/<id>]
|
account | The person's other accounts and the import with a sign-in to Gmail or Outlook.com; addresses elsewhere proved with a code and sent as; masked addresses. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/me/subscriptions, /api/v1/me/brand-logo, /api/v1/me/recoverable[/<id>[/restore]], /api/v1/me/confidential[/<id>/revoke], /api/v1/me/offline[/messages], /api/v1/me/lists[/<address>], POST /api/v1/me/recall, GET /api/v1/me/export.zip
|
account | Subscriptions and a sender's BIMI logo; deleted items put back; messages that expire; the offline window; managed lists joined; a message recalled from the accounts on this server that have not read it; everything held for the account as one archive. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/me/recovery[/codes|/resend], /api/v1/me/openpgp[/<fingerprint>|/publish], /api/v1/me/openpgp/keys/..., /api/v1/me/openpgp/lookup, /api/v1/me/protected[/<id>[/content]], /api/v1/me/zero-access[/rewrap], /api/v1/me/smime/enroll, /api/v1/me/smime/issued/<serial>[/confirm]
|
account (never an app password for the recovery, key and zero-access changes) | The recovery address and one-time codes; the keys the server publishes and the webmail's own OpenPGP ring; password-protected messages; zero-access; a key signed by the domain's S/MIME authority. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/me/devices[/<id>], /api/v1/me/push[/subscriptions[/<id>]|/test], /api/v1/me/sender-list[/<id>], /api/v1/me/quarantine-digest
|
account | The clients that have signed in, and revoking one; Web Push; the reader's own safe and blocked senders; the quarantine digest. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/me/preferences, /identities, /storage, /files[/<id>[/content]], /scheduled[/<id>], /drafts/<id>/schedule, /folders/<id>[/empty|/export], /messages/<id>/html|receipt|snooze|source|unsubscribe, /app-passwords/<id>, /sessions[/<id>], /smime[/chain|/own|/recipients]
|
account | The webmail's own routes of 6.3.3; APIs Reference §4 has each. |
GET / POST / DELETE
|
/api/v1/passkeys[/<id>], POST /api/v1/passkeys/options; POST /api/v1/passkeys/challenge, GET /api/v1/signin-methods
|
the credential a session answers to (an API key is refused); the last two need none | Passkeys enrolled and removed; a passkey sign-in's challenge; what the sign-in page may offer. 6.3.4. |
POST / GET
|
/api/v1/password-reset[/complete], /portal/reset, /recovery/verify
|
none | A forgotten password, answered 202 in one sentence whatever the address; the reset page; a recovery address confirmed. 6.3.4. |
GET / POST
|
/portal/oidc/start, /portal/oidc/callback, /portal/saml/start, /portal/saml/acs, /portal/saml/finish, /saml/metadata
|
none | Single sign-on by OpenID Connect or SAML 2.0 into the webmail and the Deck, ending in a one-use ticket for POST /api/v1/session. 6.3.4.
|
GET / POST
|
/confidential/<token>, /api/v1/protected/<token>[/reply], /portal/protected/<token>, /lists/...
|
none | The reading pages of a message that expires and of a password-protected one; the list manager's pages. 6.3.4. |
GET |
/api/v1/help, /api/v1/help/<page>
|
none | The documents built into the server, behind every help button. 6.3.4. |
GET / POST / DELETE
|
/api/v1/setup/checklist, /api/v1/setup/sample-domain
|
admin, key; writes a full key | The first-hour checklist judged by the server; the sample domain made and removed. 6.3.4. |
GET / POST / DELETE
|
/api/v1/server/drain; POST /api/v1/diagnostics/run
|
admin, key (never domain-restricted); writes a full key | Maintenance mode entered, read and left; the diagnostic tests run. 6.3.4. |
GET |
/api/v1/server/settings/pending, /api/v1/settings/changed
|
administrator password only | The saved settings not yet running; what this server has changed from stock. 6.3.4. |
GET / POST
|
/api/v1/settings/environment, /api/v1/settings/levels, /api/v1/settings/profiles[/<name>[/apply]|/revert|/discard|/decline]
|
admin, key (never domain-restricted); writes a full key | What the environment manages; the settings with levels; the four profiles, a diff, an apply with its confirmation, and the way back. 6.3.4. |
POST |
/api/v1/audit/<id>/revert |
administrator password, or a role holding manage_settings; never a key |
A setting put back from its audit row. 6.3.4. |
GET / POST / DELETE
|
/api/v1/administrator/second-factor, POST /api/v1/addresses/erase, /api/v1/accounts/<address>/export[.zip]
|
administrator password only (reading the second factor: any key) | The administrator's second factor; an address's traces erased; a mailbox or everything held on an account exported. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/accounts/<address>/rules[/<id>], /api/v1/domains/<name>/rules[/<id>]
|
admin, key (the account's or domain's own); writes a full key | An account's own rules, and a domain's own. 6.3.4. |
GET / PUT / POST / DELETE
|
/api/v1/accounts/<address>/directory[/photo], .../address-books[/<id>/shares/<target>], .../recovery[/codes], .../devices[/<id>], .../push, .../openpgp[/<fingerprint>], .../move, .../password, .../unlock, .../support-session
|
admin, key scoped to the address's domain; writes a full key | An account's directory entry, address books, recovery, clients, Web Push, published keys, rename or move, a password reset and an unlock; a support session is 6.3.3's, the rest 6.3.4. |
GET / PUT / POST / DELETE
|
/api/v1/review[/<id>[/release]], /api/v1/accounts/<address>/review[/unlock]
|
administrator password, or a role holding the permission; never a key | Messages held by compromised-account detection; an account's state and its lock. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/roles[/<name>], /api/v1/role-assignments[/<address>], /api/v1/message-trace[/address/<address>|/queue/<id>]
|
admin, key; writes a full key | Administrator roles and who holds them; the message trace. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/sender-list[/<id>], /api/v1/domains/<domain>/sender-list[/<id>], /api/v1/greylisting-bypass[/<id>], /api/v1/domain-policy-list[/<id>], /api/v1/journal-rules[/<id>]
|
admin, key (the server's lists never domain-restricted); writes a full key | The server's and a domain's allow and block lists; the greylisting bypass list; the domain white and black list; journaling and dual-delivery rules. 6.3.4. |
GET / POST / DELETE
|
/api/v1/domains/<domain>/dkim/rotation[/start|/check|/switch|/retire|/next], /api/v1/dkim/rotation/run, /api/v1/domains/<domain>/bounces, .../suppressions[/<address>], /api/v1/bounces/classify|expire, /api/v1/domains/<domain>/complaints, /api/v1/complaints/rates|expire, /api/v1/domains/<domain>/spam-traps[/hits], /api/v1/spam-traps/<address>, /api/v1/spam-traps/hits, /api/v1/reputation[/check], /api/v1/session-policy, /api/v1/domains/<domain>/session-policy
|
admin, key; writes a full key | DKIM rotation, bounces and the suppression list, complaints, spam traps, the reputation watch and the session policy. 6.3.4. |
GET / POST / DELETE
|
/api/v1/domains/<domain>/dmarc-reports[/summary|/<id>], .../tls-reports, /api/v1/domain-reports/expire, /api/v1/domains/<domain>/smime-ca[/rotate|/issued/<serial>]
|
admin, key scoped to the domain; writes a full key | The DMARC and TLS-RPT reports a domain receives; a domain's S/MIME authority. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/webhooks[/<name>|/deliveries|/run|/test] |
administrator password, or a role holding the permission; never a key | Outbound webhooks and their delivery queue. 6.3.4. |
GET / POST
|
/api/v1/lists/<address>/members[/<member>/approve|/reset-bounces], /api/v1/lists/<address>/archive[/<id>]
|
admin, key scoped to the list's domain; writes a full key | Who a list delivers to now; a request to join approved; a member's bounces cleared; a managed list's archive. 6.3.4. |
GET / POST / PUT / DELETE
|
/api/v1/logs/sessions[/<id>], /api/v1/queue/shaping[/<destination>], /api/v1/sandbox/held[/<queue_id>[/release]], /api/v1/warmup[/<id>[/history]]
|
admin, key (never domain-restricted); writes a full key | A connection followed live; outbound shaping's state; messages held for a sandbox; IP warm-up schedules. 6.3.4. |
GET / POST / DELETE
|
/api/v1/migrations[/<id>[/start|/pause|/resume|/summary.csv|/jobs[/<job>[/retry|/cancel|/signin|/report]]]] |
admin, key; writes a full key | A migration of jobs, one per mailbox, from IMAP, Google Workspace or Microsoft 365. 6.3.4. |
GET / POST
|
/api/v1/backup/archives, /api/v1/backup/<archive>/mailboxes/<address>[/restore], /api/v1/backup/restore-mailbox/<job>, POST /api/v1/backup/restore
|
admin, key (never domain-restricted); writes a full key | The archives; one mailbox or one message restored, and the job's outcome; a whole archive restored. 6.3.4. |
POST / GET
|
/api/v1/send, /api/v1/send/<queue_id>
|
an API key naming a sender and holding send_through_api - nothing else |
Mail sent as an application, and what became of each recipient. 6.3.4. |
GET / POST / PUT / PATCH / DELETE
|
/scim/v2/Users[/<id>], /scim/v2/Groups[/<id>], /scim/v2/ServiceProviderConfig, /scim/v2/Schemas, /scim/v2/ResourceTypes
|
an API key holding provision - nothing else |
SCIM 2.0 provisioning of accounts, aliases and lists by the organisation's identity provider. 6.3.4. |
GET / POST
|
/api/v1/discovery/cases, /api/v1/discovery/cases/<id>, POST .../<id>/close
|
a role holding view_discovery to read (the auditor role holds it), manage_discovery to change (serveradmin); the administrator password; a key whose role holds them |
eDiscovery: the cases, one case whole with its searches, holds and exports, and a case closed with every live hold released. On the audit trail. 6.3.4 (schema 6097) |
POST / GET
|
/api/v1/discovery/cases/<id>/searches, .../searches/<search>, POST .../searches/<search>/export, GET .../exports/<export>
|
as above; a search or export that read the archive also needs view_archive, and a download view_mailbox
|
A search of every named mailbox, its deleted items and the archive by person, date, words and attachment, queued and paged; its hits exported as EML and mbox in a zip with a manifest of SHA-256s; the zip downloaded (413 past 500 MB). 6.3.4 |
POST / DELETE
|
/api/v1/discovery/cases/<id>/holds, .../holds/<hold>
|
manage_discovery; a confined credential holds nothing outside its domains |
A legal hold on an account, a domain or the archived copies a search matched - it stops deletion, retention, a purge, a move to another domain and a full restore - and its release. 6.3.4 |
GET / POST / PUT / DELETE
|
/api/v1/inbound-webhooks, /api/v1/inbound-webhooks/<address>
|
view_configuration to read; manage_inbound_webhooks to change (only serveradmin holds it as shipped, and a key whose role holds it); scoped to the route's domain |
Mail in to a webhook: an address, or a whole hosted domain, routed to a URL, the mail that arrives posted there as SendGrid-style form fields or one JSON document, signed. The URL is answered whole only to a credential that may change it; the secret never. 6.3.4 (schema 6100) |
GET / POST / DELETE
|
/api/v1/inbound-webhooks/deliveries, .../deliveries/<id>, POST .../deliveries/<id>/retry, GET .../deliveries/<id>/message, POST /api/v1/inbound-webhooks/run
|
as above; the kept message also needs view_mailbox; run a credential that reaches every domain |
The queue of posts, one removed or tried again, the message a delivery kept, and one pass now. 6.3.4 |
GET / PUT / DELETE / POST
|
/api/v1/accounts/<address>/mailbox-encryption, POST .../mailbox-encryption/recover, /api/v1/domains/<domain>/mailbox-recovery
|
admin, key scoped to the account's or the domain's own domain; writes a full key | Mailbox encryption at rest: the account's state, switched on or off, its key forfeited (?confirm=forfeit), a forgotten password recovered with the domain's passphrase; the domain's recovery key made, replaced or removed. On the audit trail. 6.3.4 (schema 6099) |
GET |
/api/v1/me/mailbox-encryption |
account | Whether the account's own mailbox is encrypted at rest; read-only. 6.3.4 |
GET / PUT
|
/api/v1/accounts/<address>/booking-policy |
admin, key scoped to the address's domain; PUT a full key | A room's or a piece of equipment's booking policy and its delegates (the account's kind is set on PUT /api/v1/accounts/<address>). 6.3.4 (schema 6101) |
GET / POST
|
/api/v1/store, POST /api/v1/store/measure
|
admin, key (never domain-restricted); measure a full key holding manage_server
|
The message store's last measurement - what the messages are, what their files take, what compression and de-duplication save - and a measurement started now. 6.3.4 (schema 6098) |
POST |
/api/v1/me/folders/<id>/bulk |
account | One action - flags, a move or a delete - over many messages of a folder, the webmail's select-all, done by the server in bounded steps and continued by before_uid. 6.3.4
|
GET / PUT / POST
|
/api/v1/portal/branding, /api/v1/portal/files, POST /api/v1/scheduled/run, POST /api/v1/settings/cache/clear, /settings/indexing/index|clear, /settings/scripting/check|reload
|
admin, key (branding readable by anyone); writes a full key | The webmail's branding and file-link policy, the send-later pass, the caches, the index and the event script. 6.3.3. POST /api/v1/settings/breach-check/test and /settings/push/vapid are 6.3.4.
|
GET / POST / PUT / DELETE
|
/api/v1/dns-blacklists, /api/v1/surbl-servers, /api/v1/whitelist-addresses, /api/v1/blocked-senders, /api/v1/incoming-relays, /api/v1/greylisting-white-addresses and /api/v1/blocked-attachments, each with /<id>; /api/v1/domains/<domain>/domain-aliases[/<name>]; POST /api/v1/sieve/evaluate
|
admin, key (the lists never domain-restricted; a domain's aliases scoped to it); writes a full key | The anti-spam lists and the blocked attachments; a domain's other names; a Sieve script tried against a message without storing anything. 6.3.3. |
GET |
/portal-lang/<code>.json, /portal-smime.js
|
none | The webmail's catalogues, and its S/MIME module. 6.3.3. |
New in 6.2.28: GET /api/v1/update and POST /api/v1/update/check|download|install
(the live update), GET /portal (the self-service sign-in page) and the account's own
routes under /api/v1/me (state, password, automatic reply, quarantine, folders,
messages, search, send, drafts, attachments, settings, Sieve filters) with
POST/DELETE /api/v1/session for browser sessions. Since 6.3.4, each folder in
GET /api/v1/me/folders also carries uidnext beside uidvalidity: the folder's current UID
plus one, which is what IMAP STATUS reports as UIDNEXT and the first REST route to report
a folder's current UID at all.
How many routes. The OpenAPI document the server emits is the count to trust. It is made of
the path literals in RestApi*.cpp and of the settings groups, whose paths RestApiSettings.cpp
writes from its group tables (OpenApiPath), so that no literal shows them: /api/v1/settings and
its named groups, each a GET and all but directories a PUT - nine paths and 17 operations in
6.3.3 and at 3233b0cc4, thirteen and 25 at master 540347500. Counted both ways together, the
document names 146 paths and 219 path-and-method operations in 6.3.3, 167 paths and 246
operations at commit 3233b0cc4, 15 September 2026 (163 and 242 of them under /api/),
417 paths and 588 operations at master 540347500, 24 September 2026, and 441 paths and 621
operations in 6.3.4 (92b8f9f63; 413 and 585 under /api/; the rest are /scim/v2, the sign-in,
reset and reading pages, the list manager's pages and the static catalogues and scripts). The path
literals alone give 137 and 202, 158 and 229, 404 and 563, and 428 and 596; the thirteen settings
groups are the same thirteen, 25 operations, in 6.3.4. The twenty-four paths 6.3.4 adds over
540347500 are the rows above tagged 6.3.4 that name eDiscovery, the inbound webhooks, mailbox
encryption at rest, the booking policy, the store and the folder's bulk action. This table is the map,
not the census; APIs Reference §4 has a row per route.
Path-parsing rules that catch people out: routing is exact-match on method and path
after the query string is stripped; ids in queue, quarantine, IP-range, archive and
/api/v1/me/... paths must be 1–18 decimal digits and greater than zero, or the route is
simply unknown and answers 404; a trailing slash is not the same path.
Every example uses the loopback, plain-HTTP shape — the one a fresh
RestApiPort=8443 + RestApiBindAddress=127.0.0.1 gives you. With
RestApiCertificateFile/RestApiPrivateKeyFile set (or an ACME certificate present),
change http:// to https://. Bodies below are the shapes the format strings in
RestApiServer.cpp produce, pretty-printed for reading; the server sends them on one line.
curl -u Administrator:your-admin-password http://127.0.0.1:8443/api/v1/status{"version":"6.2.28","state":3,"processedMessages":10412,"spamMessages":318,
"virusesRemoved":4,"sessions":{"smtp":2,"imap":11,"pop3":0}}state is 0 Unknown, 1 Stopped, 2 Starting, 3 Running, 4 Stopping. There is no
uptime field, whatever the OpenAPI summary says.
curl -u Administrator:your-admin-password http://127.0.0.1:8443/api/v1/domains[{"name":"example.com","active":true},{"name":"example.net","active":true}]# List
curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains/example.com/accounts
# Create - only "address" and "password" are read; the account is created active,
# with its INBOX, and the password is hashed with PreferredHashAlgorithm
curl -u Administrator:pw -H "Content-Type: application/json" \
-d '{"address":"newuser@example.com","password":"S0me-Long-Passphrase"}' \
http://127.0.0.1:8443/api/v1/domains/example.com/accounts
# Delete
curl -u Administrator:pw -X DELETE \
http://127.0.0.1:8443/api/v1/accounts/newuser@example.com{"address":"newuser@example.com","created":true}Failure shapes: 400 when a field is missing, when the address is not in the named
domain, or when the account limit check refuses it (its own message is passed through);
404 {"error":"domain not found"}; 409 {"error":"account already exists"}.
curl -u Administrator:pw http://127.0.0.1:8443/api/v1/queue{"count":1,"messages":[
{"id":81234,"created":"2026-09-08 09:12:44","from":"sales@example.com",
"recipients":"buyer@slow-host.example","next_try":"2026-09-08 10:12:44",
"locked":false,"tries":3}
]}curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/queue/81234/retry # {"retried":true}
curl -u Administrator:pw -X DELETE http://127.0.0.1:8443/api/v1/queue/81234 # {"deleted":true}Retry resets the next-try time and kicks delivery. A 404 means the id is not a message
in Delivering state and not one held for ETRN — including an id that has just been
delivered.
curl -u Administrator:pw http://127.0.0.1:8443/api/v1/tlsa
curl -u Administrator:pw http://127.0.0.1:8443/api/v1/srv/api/v1/tlsa returns one record per configured certificate file, already formatted:
_25._tcp.<host>. IN TLSA 3 1 1 <hex>. /api/v1/srv derives _imaps, _imap,
_pop3s, _pop3, _submissions and _submission from the configured TCP/IP ports
(port 25 is never advertised, loopback-bound ports are skipped), adds
_autodiscover._tcp when the web-services HTTPS listener is actually running and
AutoconfigEnabled=1, and emits a record set for every active domain.
curl -u Administrator:pw http://127.0.0.1:8443/api/v1/logs
curl -u Administrator:pw "http://127.0.0.1:8443/api/v1/logs/hmailserver_2026-09-08.log?lines=50"
curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/backup # 202 {"started":true}
curl -u Administrator:pw http://127.0.0.1:8443/api/v1/backup # poll for statuslines defaults to 200 and is capped at 2000. The name must be a bare *.log file name
with no path — anything else is 400 {"error":"not a log file name"}.
# What the mailbox owner sees about themselves
curl -u alice@example.com:her-password http://127.0.0.1:8443/api/v1/me{"address":"alice@example.com","domain":"example.com","active":true,
"quota":{"limit_mb":250,"used_bytes":48213004},
"vacation":{"enabled":false,"active":false,"subject":"","message":"",
"expires":false,"expires_date":""},
"password_changed":"2026-06-02 08:31:10","second_factor":false,"directory_linked":false}# Turn an automatic reply on. PUT takes the whole state at once.
curl -u alice@example.com:her-password -X PUT \
-H "Content-Type: application/json" \
-d '{"enabled":true,"subject":"Away","message":"Back on the 20th.",
"expires":true,"expires_date":"2026-09-20"}' \
http://127.0.0.1:8443/api/v1/me/vacation
# Change the password. "current" must be the account password itself,
# not an app password, even if an app password is what signed you in.
curl -u alice@example.com:her-password -X POST \
-H "Content-Type: application/json" \
-d '{"current":"her-password","new":"a-better-passphrase"}' \
http://127.0.0.1:8443/api/v1/me/passwordPresent the administrator password or an API key to any of these and you get
403 {"error":"this endpoint answers to an account's own credentials, not to the administrator password or an api key"}.
Present an account's credentials anywhere else and you get
403 {"error":"an account's credentials reach only the account's own endpoints under /api/v1/me"}.
# Sign in; keep the cookie
curl -c cookies.txt -u alice@example.com:her-password \
-X POST http://127.0.0.1:8443/api/v1/session
# {"address":"alice@example.com","idle_seconds":1800,"lifetime_seconds":43200}
# Read with the cookie alone
curl -b cookies.txt http://127.0.0.1:8443/api/v1/me
# Write with the cookie - the CSRF header is required
curl -b cookies.txt -X PUT -H "X-Requested-With: hMailServer" \
-H "Content-Type: application/json" -d '{"enabled":false}' \
http://127.0.0.1:8443/api/v1/me/vacation
# Sign out
curl -b cookies.txt -X DELETE -H "X-Requested-With: hMailServer" \
http://127.0.0.1:8443/api/v1/sessioncurl -u Administrator:pw http://127.0.0.1:8443/api/v1/update # the verdict
curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/update/check
curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/update/download
curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/update/installstateDiagram-v2
[*] --> NotChecked
NotChecked: 0 not checked since the service started
UpToDate: 1 the running version is the latest on the channel
Available: 2 a newer release exists
Downloaded: 3 its installer is downloaded and Sigstore-verified
Installing: 4 handed to hMailServer.Updater.exe
Failed: 5 the last check failed, lastError says why
NotChecked --> UpToDate: check finds nothing newer
NotChecked --> Available: check finds a newer release
NotChecked --> Failed: the feed could not be read
UpToDate --> Available: a later check
Available --> Downloaded: POST /update/download verifies it
Available --> Failed: download or verification failed
Downloaded --> Installing: POST /update/install
Installing --> [*]: the service stops and starts
Failed --> Available: a later check succeeds
Source: Common/Util/UpdateChecker.h state enumerators; the same numbers appear on COM
Status.UpdateState; since 6.3.6 two more, 6 while the service prepares an administrator's
install and 7 when it is ready, and where the service runs as NT SERVICE\hMailServer,
POST /update/install answers 409 update_install_by_administrator - Install update in the
Control Panel on the server installs it, with one Windows prompt. POST /update/check reads the feed whether or not
UpdateCheckEnabled is on. POST /update/download fetches the installer and its
Sigstore bundle into <DataDirectory>\Updates, verifies them, and deletes anything that
fails — nothing is run. Only POST /update/install runs it, and the service stops and
starts during the update.
6.3.4, the install drains first: it enters maintenance mode
(POST /api/v1/server/drain does the same by hand, and DELETE leaves it), so the listeners are
closed and delivery paused before the helper stops the service, and it refuses with "already
draining" while somebody else's drain is in progress. A rolled-back update's outcome is reported as
soon as the service is back. On Linux the install applies too: the server writes a request into
/run/hmailserver, and a root path unit runs /usr/lib/hmailserver/hmailserver-update, which
verifies the package again as root, installs it and rolls back when the service does not come back
(Installing on Linux). Since 6.3.4 the default feed is https://updates.progressiverobot.com, which
GitLab Pages serves from the project's releases. Up to 6.3.3 it is
https://api.github.com/repos/Progressiverobot/hmailserver/releases/latest, which has answered 404
since GitHub suspended the project's account on 18 September 2026, so such a server's check fails with
The update feed answered HTTP 404. until UpdateFeedUrl names
https://updates.progressiverobot.com/releases.json.
| Code | When | Body | What to do |
|---|---|---|---|
| 200 | The call worked | route-specific JSON | — |
| 201 | Something was created: an account, a list, an IP range, a key, a draft, a session | the created thing | — |
| 202 |
POST /api/v1/backup accepted; it runs on the maintenance queue |
{"started":true} |
Poll GET /api/v1/backup
|
| 400 | Malformed request line, a NUL byte anywhere, a missing or bad JSON field | {"error":"..."} |
Read the message; it names the field |
| 401 | No credential, wrong password, unknown/expired/out-of-network key — deliberately indistinguishable. Also any key touching /api/v1/apikeys, and since 6.3.4 any key on the other routes that answer to the administrator password alone (section 2); on /scim/v2 any credential but a key, in SCIM's shape |
{"error":"authentication failed"} |
Check the credential; check the log, which says which kind failed |
| 403 | Authenticated but not permitted: read-only key on a mutating route, domain-restricted key out of its domains or on a server-wide route, wrong credential class for /api/v1/me, missing CSRF header |
{"error":"<the reason>"} |
The reason is a fixed sentence and is accurate |
| 404 | Unknown route, or a named object that does not exist |
{"error":"not found"} or e.g. {"error":"account not found"}
|
Check the path shape and the id rules |
| 409 | It already exists, or the state forbids it (account exists, a backup is running, a directory-linked password, a recently used password) | {"error":"..."} |
— |
| 411 | The request used Transfer-Encoding — a body with no declared length |
{"error":"length required"} |
Send Content-Length; this server does not de-chunk |
| 413 | Head + body over 64 KB, or over 16 MB on the two routes that carry attachments | {"error":"request too large"} |
Split the request; the connection is closed |
| 429 | Over 200 requests in the current 10-second window for this credential |
{"error":"too many requests"} with Retry-After: 10
|
Wait the window out; it is fixed, so waiting works |
| 500 | A handler threw, or the server could not do the thing |
{"error":"internal error"} or a specific message |
Check the application log |
| 503 | A subsystem is not running (the backup manager) | {"error":"..."} |
— |
100 Continue is sent when a client asks for it and the declared length has already
passed the cap. Any other status a handler tried to return is rewritten to 500 —
deliberately, because a wrong number in a response line is worse than an honest server
error.
Every API response carries Content-Type: application/json and Cache-Control: no-store.
The exceptions are the static pages /, /index.html, /portal, /portal.js and the
attachment download, which serve their own media types and are also no-store.
| Limit | Value | Applies to | Symptom |
|---|---|---|---|
| Request size | 64 KB head + body | every route | 413, connection closed |
| Large-request exception | 16 MB and a 300 s deadline |
POST /api/v1/me/messages, POST /api/v1/me/drafts only |
413 above that |
| Request deadline | 30 s absolute, from when the server starts waiting to the last byte out; the TLS handshake counts | every other route | connection closed with no response |
| Connection lifetime | 300 s, whatever it is doing | every connection | connection closed |
| Requests per connection | 1000 | keep-alive clients | the connection is closed after the 1000th response |
| Concurrent connections | 64, enforced at accept before the TLS handshake | the listener | connection closed; the log says how many have been refused |
| Worker threads | 4 | the listener | requests queue |
| Rate | 200 requests per fixed 10 s window per credential | each credential separately | 429 + Retry-After: 10; one log line per credential per window |
| Auto-ban | shared with SMTP/IMAP/POP3 accounting; loopback peers exempt | failed authentications | after the ban trips, the address is refused at accept for 5 minutes |
The rate budget is per credential and not per source address, on purpose: rotating source addresses must not multiply what one leaked key can spend, and two honest keys behind one NAT must not be able to starve each other.
| Symptom | Most likely cause | Check |
|---|---|---|
| Connection refused, nothing in the log about the REST API |
RestApiPort is still 0, or the service was not restarted after the edit (since 6.3.4 the key is marked reinitialize: Apply now, or POST /api/v1/server/reinitialize) |
The startup log; RestApi: Listening on ... is the only proof it is up |
Refusing to start - the administrator password is not set |
Fresh install with no administrator password | Set one, then restart |
Refuses to start on 0.0.0.0
|
TLS is mandatory off loopback | Set the certificate pair, enable ACME, or bind 127.0.0.1
|
It started on 0.0.0.0 after ACME issued |
Expected. ACME reinitialises the application after writing fullchain.pem, and the REST listener picks it up then |
RestApi: Using the ACME certificate for HTTPS. |
| 401 on a key that worked yesterday | The default lifetime is 90 days and there is no "never expires" |
GET /api/v1/apikeys shows expired per key; the log names the label |
| 401 from a key that is definitely correct | It is being presented from an address outside AllowedFrom, or the store record's Hash is not 64 lower-case hex and is being ignored |
The application log distinguishes these; the response never does |
403 this api key is read-only
|
Scope defaults to read-only when a create request does not name one, and any Scope value other than the literal full reads as read-only |
Re-issue with "scope":"full"
|
403 on /api/v1/queue from a working key |
The key has a Domains list; the queue is server-wide and is refused outright rather than narrowed wrongly |
Use an unrestricted key for queue work |
401, not 403, when a key touches /api/v1/apikeys
|
Deliberate — a key must learn nothing about key management, not even whether a verb exists | Use the administrator password |
403 with the X-Requested-With message from a browser |
The request is cookie-authenticated and changes something | Add X-Requested-With: hMailServer
|
429 with Retry-After: 10
|
200 requests in ten seconds on one credential | Slow the poll down; a once-a-second status poll is 5% of the budget |
| Sudden "connection refused" after several bad passwords | The auto-ban tripped and the address is refused at accept for five minutes | Wait, or connect from loopback, which is exempt |
- APIs Reference — every route with its request and response shape, the API key store format and its fail-closed rules, the web-services and metrics listeners, and the COM object model.
-
Settings Reference —
RestApiPort,RestApiBindAddress,RestApiCertificateFile,RestApiPrivateKeyFileand every other key, with defaults and timing. - Settings Overview — the same keys in the shape an administrator edits them.
# Create - the name is checked as the Control Panel checks it (a valid domain name,
# not one a domain alias already has); every other setting takes the default a new
# domain gets there. "active" defaults to true; "postmaster" is optional.
curl -u Administrator:pw -H "Content-Type: application/json" \
-d '{"name":"example.com","active":true,"postmaster":"postmaster@example.com"}' \
http://127.0.0.1:8443/api/v1/domains
# Switch off (and on again); "postmaster" changes only when it is named
curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \
-d '{"active":false}' http://127.0.0.1:8443/api/v1/domains/example.com
# Delete, with everything in it
curl -u Administrator:pw -X DELETE http://127.0.0.1:8443/api/v1/domains/example.com{"name":"example.com","active":true,"postmaster":"postmaster@example.com"}Failure shapes: 400 when name (POST) or active (PUT) is missing, or when the save is
refused (the Control Panel's own sentence is passed through: "The domain name you have
entered is not a valid domain name.", "A domain alias with this name already exists.");
404 {"error":"domain not found"}; 409 {"error":"domain already exists"}. Creating and
deleting a domain are server-wide: a key restricted to named domains is refused with 403
even for its own name, as is a read-only key for all three. The name cannot be changed
over the API; renaming stays with COM. Released in 6.3.0.
hMailServer 6.3.7, the latest release (17 October 2026, schema 6108) · AGPL-3.0-or-later · Repository · Mirror on GitHub · Report a documentation error (a GitLab issue naming the page)
Home — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- Profiles and Environment
- The Control Panel
- The Control Deck
- Your First Domain and Mailbox
- Connecting a Mail Client
- The Webmail
- DNS for Your Domain
- Migration
2. Secure it
- Encryption and Certificates
- Database TLS
- Encryption for People
- Stopping Spam
- Stopping Viruses
- Security Hardening
- Secure Defaults
- The Service Account
- Secrets at Rest
- Sign-in and Identity
- Security Policy
3. Operate it
- Monitoring and Health
- The SNMP Agent
- Backup and Restore
- Message Storage
- Compliance and eDiscovery
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- The Update Feed
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Rooms and Equipment
- Mailing Lists
- Routes and Relays
- Mail In to a Webhook
- Outbound Shaping
- The COM API and Scripting
- Event Scripts
- The REST API
- Command Line
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey
- Settings Reference
- Settings Overview
- Capabilities and Configuration
- Control Panel Reference
- Control Panel Pages
- Ports Reference
- Changes-Since-6210
- Version-History-to-6210
- Glossary
About this wiki
- hMailServer 6.3.7 is the latest release (17 October 2026, schema 6108)
- Pages say since 6.3.4 where a feature is new in it
- AGPL-3.0-or-later
- Repository, the one place the project is worked on
- Its mirror on GitHub, since 18 October 2026, with no Issues
- Questions: a GitLab issue with the Question template, GitHub Discussions Q&A, or the forum
- Report a documentation error: a GitLab issue naming the page