The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples
-
Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) changes how the listener judges some requests. A state-changing request that carries a credential, HTTP Basic included, is refused when the browser reports it as cross-site (
Sec-Fetch-Site, or anOriginthat is not this server's); scripts (thecurlexamples below among them),hmctland the PowerShell module send no such header and are unaffected. A credential confined to named domains (a domain administrator, a key issued for named domains, orserveradmingiven with a list of domains) is refused a domain's rename, a domain alias, a custom role's creation, change or delete, thebind_to_address,send_using_routeandscript_functionactions on a domain's or an account's rule, a relay host changed to an address that is not public, andGET /api/v1/metrics/history; and a role is given or taken away only by a credential that holdsmanage_rolesin the account's domain and takes in what the account holds now and afterwards, so such a credential cannot take a role away from an account whose admin level is the server administrator's. The OpenAPI document describes each path once, with every method it answers. In the Control Panel, API & monitoring is Monitoring & alerts and REST API keys is under Sign-in & access; Ctrl+K takes every old title to its page. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) reads a JSON body in time and memory that follow its bytes (work item 110), and answers 400, saying why, before a route reads a member, to a body that is not one JSON object, names a member twice, holds an escaped U+0000 or passes 100,000 values or 32 levels, and to a member whose absence is the wider setting written as another type than its route reads, such asdomains,allowed_from,roleorexpiresonPOST /api/v1/apikeysas anything but a string or null: correct a client of your own that sends such a body. An account's failed sign-in from loopback, as through a reverse proxy on the same machine, is counted against the name alone,AccountLockoutThreshold(off by default) being the limit, and never towards a ban of the address (work item 107); the listener's own accounting spares all of 127.0.0.0/8. See the 6.3.8 announcement.The REST API is the alternative to COM for automating hMailServer, and the only administration interface that works from a machine that is not running Windows. This guide covers how to turn the listener on, the three credentials it takes and the roles that narrow them, how one-time codes behave since 6.3.4, the OpenAPI document the server emits, and a set of
curlexamples that use only routes the project's own documents name. The one thing to know first: the listener is off as shipped (RestApiPort=0), and since 6.3.4RestApiPortis a setting stored in the database. Editing the[Settings]section ofhMailServer.inidoes not turn the API on once the server has stored the key.Turning the listener on
Four settings shape the listener.
Setting Shipped value What it is RestApiPort0The port. 0means there is no listener.RestApiBindAddress127.0.0.1The address it binds. TLS is required unless this is 127.0.0.1,localhostor::1.RestApiCertificateFileempty The certificate, in PEM. Left empty, the listener falls back to the ACME certificate. RestApiPrivateKeyFileempty Its private key, in PEM. Since 6.3.4 the database is the settings store (schema 6042) and the
[Settings]section ofhMailServer.iniis its copy. A key the server has never stored is taken in from the file at the next start, so on a fresh installation an edit made before the first start still works; once the key is stored, the database's value is the one used. Settings: what is not stock, when a save takes effect covers the store itself, why an edited line is put back (HM5804) and[SettingsOverride]. Where the stored value is changed depends on the platform.Platform How RestApiPortis setWindows The Control Panel page Monitoring & troubleshooting → API & monitoring, or Settings.SetIniSettingover COM. The Control Panel and the COM API are Windows-only.Linux hmailserver --set-settingon the server itself, then a restart of the service.Either, once the API answers PUT /api/v1/settings/ini/{name}with the administrator password.sudo hmailserver --set-setting RestApiPort=8443 sudo systemctl restart hmailserver--set-settingchecks what it is given: a port must be a number from 0 to 65535 andRestApiBindAddressan IP address orlocalhost.On Linux this matters most, because there 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, and changing those lines afterwards turns nothing on. The one part of the file that still decides a stored key is a[SettingsOverride]section, applied over the store at every start and reported asHM5807each time; the wiki gives it as the route where neither the Control Panel nor COM is available.The new value waits for a reinitialise. Since 6.3.4 most settings apply when they are saved, but
RestApiPort,RestApiBindAddress,RestApiCertificateFileandRestApiPrivateKeyFileare among the 28 keys markedreinitialize, because the listener binds and its TLS context is built when the servers start. They take effect at Apply now in the Control Panel (Windows) or the Control Deck, atApplication.Reinitializeover COM (Windows), atPOST /api/v1/server/reinitialize, or at a restart of the service.The listener can refuse to start, and each refusal is written once to the application log.
What is wrong Log line Fix The administrator password is empty RestApi: Refusing to start - the administrator password is not set.Set an administrator password The bind address is not loopback and there is no certificate RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile.Set both files, enable ACME, or bind loopback The 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 byHM5113naming the certificate or key fileCorrect the PEM paths and permissions Nothing RestApi: Listening on 127.0.0.1:8443 (http, loopback only).or... (https).None needed The
Listening online is the only proof that the listener is up. The loopback exemption is a string comparison against127.0.0.1,localhostand::1;127.0.0.2and::ffff:127.0.0.1do not qualify. The TLS configuration is the one the mail protocols use (cipher list, protocol switches,TlsKeyExchangeGroups), with a TLS 1.2 minimum applied on top, so opening an older protocol for a mail client does not open it for the management API.The three credentials
Credential Header Reaches Administrator password Authorization: Basicwith the user nameAdministrator(case-insensitive)Every administrative route. It is never read-only and never confined to domains. API key Authorization: Bearer hmapi_<64 hex>What its scope, role and domains allow, less the routes listed below. An account's own Authorization: Basicwith the address and the mailbox password or an app password, or thehmailsessioncookie/api/v1/meand/api/v1/session; since 6.3.4 also the administrative routes a role it holds allows.The two Basic forms are told apart before either password is tried: any user name but
administratoris treated as a mailbox address and goes down the path an IMAP logon takes, with the same lockout and auto-ban. An account is never tested against the administrator password.API keys are the credential for scripts. A key is read-only unless it is made with
"scope":"full", expires after 90 days unless the request names anexpires(there is no "never expires"), and may be narrowed to named domains and to a source address, range or CIDR. Only the SHA-256 of a key is stored, inhMailServerApiKeys.inibesidehMailServer.ini, and that file is read again at every authentication attempt, so removing a key's section revokes it on the next request.# Administrator password only: a key can never make 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"}keyis the clear text. It is returned once and cannot be recovered, so store it then.GET /api/v1/apikeyslists the keys with anexpiredflag and never a hash;DELETE /api/v1/apikeys/<id>revokes one. The create request takeslabel(required, at most 64 characters),scope,domains,expiresandallowed_from, and since 6.3.4roleand the send API'ssend_from,send_per_minuteandsend_per_day.What no API key reaches. Whatever its scope or role, a key is answered
401(not403, so that it learns nothing) on the key store/api/v1/apikeysand on/api/v1/settings/ini, and since 6.3.4 also on/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. In the other direction, since 6.3.4 a key is the only credentialPOST /api/v1/sendtakes (a key naming a sender) and the only one/scim/v2takes (a key whose role holdsprovision).Roles, since 6.3.4
A role is a named set of permissions, and an assignment gives an account a role in named domains (schema 6047). Nothing changes on upgrade: an account with no assignment holds what its admin level has always given it, and a key without a role is what it always was:
Scope=fullisserveradminandScope=readonlyisauditor. A key is given a role withroleonPOST /api/v1/apikeys; an account is given one at/api/v1/role-assignmentsby a credential holdingmanage_roles.What a refusal says. A route a role may not call answers
403naming the role and the permission, for examplethe role 'helpdesk' does not hold the permission 'manage_settings'. An address in a domain the role does not reach answersthe role 'helpdesk' does not reach that domainwhether or not the account exists. A request authenticated with an app password never carries the account's role.Administrator roles end to end has the built-in roles and their permissions, giving a role on each surface, custom roles, and the two stores,
/api/v1/apikeysand/api/v1/settings/ini, that answer to the administrator password alone, not even to an account holdingserveradmin.One-time codes
When a second factor is enrolled on the administrator credential, the code travels in an extra header:
curl -u Administrator:your-admin-password \ -H "X-hMailServer-OTP: 123456" \ http://127.0.0.1:8443/api/v1/statusWithout it the answer is the one
401on this listener that says what is missing: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; a wrong code is.
Since 6.3.4 a one-time code is taken once, over COM and over REST, for the administrator and for an account alike. A code the server has accepted is refused if it comes again. The consequence for automation is direct: a script that sent the administrator's password and a code with every request now has only its first request in each half minute accepted. The others are refused with
401and are not counted as wrong codes, so such a script does not lock the administrator out. The changelog names the two remedies: sign in once withPOST /api/v1/sessionand use thehmailsessioncookie from there, or use an API key.# Sign in once; the password and the code travel on this request alone curl -c cookies.txt -u Administrator:your-admin-password \ -H "X-hMailServer-OTP: 123456" \ -X POST http://127.0.0.1:8443/api/v1/session # Reads carry the cookie curl -b cookies.txt http://127.0.0.1:8443/api/v1/status # Anything that changes something also carries the CSRF header curl -b cookies.txt -X POST -H "X-Requested-With: hMailServer" \ http://127.0.0.1:8443/api/v1/queue/81234/retry # Sign out curl -b cookies.txt -X DELETE -H "X-Requested-With: hMailServer" \ http://127.0.0.1:8443/api/v1/sessionThe session rules are enforced by the server: 30 minutes idle and 12 hours from sign-in until a session policy sets others (since 6.3.4,
session_idle_secondsandsession_lifetime_seconds), at most 1000 sessions, and every session dropped when the listener stops. A cookie-authenticated request that is notGETorHEADwithoutX-Requested-With: hMailServeris refused with403. Somebody who is asked for a code twice within half a minute, to sign in to the Control Deck and then to register a passkey for example, waits for the authenticator's next code.Two further changes in 6.3.4 belong here. A wrong code is counted against a budget of five in fifteen minutes, after which no code is taken for fifteen minutes; on the administrator's Basic sign-in the answer is then
429withcodesecond_factor_lockedandRetry-After. And for an account, one of its one-time recovery codes, inX-hMailServer-Recovery-Code, is taken wherever the authenticator's code is asked for.The administrator signs in at
POST /api/v1/sessionwith HTTP Basic, and with the code inX-hMailServer-OTPwhen a second factor is enrolled: AccountRecovery.md names the header for this route, the 6.3.4 changelog gives the session as the remedy above, and the Control Deck signs in that way. The wiki's credentials table, which lists/api/v1/sessionamong the routes the administrator password does not reach, is out of date on this.How a request is judged
The server tries Bearer, then Basic, then the session cookie. It then applies the CSRF check for cookie-authenticated writes (and, since 6.3.7, refuses any state-changing request that carries a credential when the browser reports it as cross-site, by
Sec-Fetch-Siteor anOriginthat is not this server's) and the rate budget of 200 requests in a fixed 10-second window per credential, turns the method and path into a route kind, and decides on that kind in one place before any handler runs.- Read-only is decided by route kind, not by HTTP method. A read-only key on a mutating route is answered
403withthis api key is read-only. - A key with a domain list is refused server-wide resources outright: the queue, the quarantine, IP ranges, certificates, rules, logs, the backup and the settings among them. On a route that names a domain or an address outside its list the answer is
403withthis api key is not permitted for that domain. - Routing is exact. Method and path must match after the query string is removed, a trailing slash is a different path, and an id in a queue, quarantine, IP-range, archive or
/api/v1/me/...path must be 1 to 18 decimal digits and greater than zero, or the answer is404.
The OpenAPI document
GET /api/v1/openapi.jsonanswers the API's description to the administrator password or to any key, a read-only one included. The wiki's advice is that this document is the count to trust: in 6.3.4 it names 441 paths and 621 path-and-method operations, 413 and 585 of them under/api/, against 146 and 219 in 6.3.3. The rest are/scim/v2, the sign-in, reset and reading pages, the list manager's pages and the static catalogues and scripts.The Control Deck draws its settings groups from this document, and since 6.3.4 the document gives the ini-backed rows of the settings groups an
x-appliesmark saying when a save takes effect. The APIs Reference describes it as a static OpenAPI 3.0.3 document and records where its text is not accurate:- the summary of
/api/v1/statusmentions uptime, and the handler returns no uptime field; - the text for
/api/v1/settings/inistill describes the file as the store, where since 6.3.4 the value goes to the database first.
Settings saved and not yet running
Since 6.3.4 a key marked
reinitializeorrestartwhose stored value is not the value the server is running on is pending. The list is recomputed at every request.Route Answer GET /api/v1/server/settings/pending[{name, stored, running, applies, secret}]; a secret's two values are(set)or emptyGET /api/v1/settings/iniThe keys as {name, value, secret}, anappliesmap and apendingarray of namesGET,PUT,DELETE /api/v1/settings/ini/{name}One key, with appliesandpendingPOST /api/v1/server/reinitialize202before it happens;{"reinitializing": true, "drained": true}when it ended a drainAll but the last answer to the administrator password alone. A
DELETEis now the only way to return a stored key to its default, because deleting a line from the file no longer does. A reinitialise restarts the REST listener with the other services, so it ends a Control Deck session; poll/api/v1/statusuntil it answers again.Worked examples
These use the loopback, plain-HTTP shape that
RestApiPort=8443andRestApiBindAddress=127.0.0.1give; put the port that was stored in place of 8443. With a certificate configured, changehttp://tohttps://. They present the administrator password on every request, which works while no second factor is enrolled; with one enrolled, use the session above or an API key.# Status: state is 0 Unknown, 1 Stopped, 2 Starting, 3 Running, 4 Stopping curl -u Administrator:pw http://127.0.0.1:8443/api/v1/status # Domains and accounts curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains curl -u Administrator:pw http://127.0.0.1:8443/api/v1/domains/example.com/accounts curl -u Administrator:pw -H "Content-Type: application/json" \ -d '{"address":"[email protected]","password":"S0me-Long-Passphrase"}' \ http://127.0.0.1:8443/api/v1/domains/example.com/accounts curl -u Administrator:pw -X DELETE \ http://127.0.0.1:8443/api/v1/accounts/[email protected] # The delivery queue curl -u Administrator:pw http://127.0.0.1:8443/api/v1/queue curl -u Administrator:pw -X POST http://127.0.0.1:8443/api/v1/queue/81234/retry curl -u Administrator:pw -X DELETE http://127.0.0.1:8443/api/v1/queue/81234 # Logs and the backup 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 curl -u Administrator:pw http://127.0.0.1:8443/api/v1/backup # With a key curl -H "Authorization: Bearer hmapi_7c1f...c92a" http://127.0.0.1:8443/api/v1/status # Store another port: it waits for a reinitialise, and the pending list shows both values curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \ -d '{"value":"8444"}' http://127.0.0.1:8443/api/v1/settings/ini/RestApiPort curl -u Administrator:pw http://127.0.0.1:8443/api/v1/server/settings/pending # Put the running port back; a value put back before anything applied it is no longer pending curl -u Administrator:pw -X PUT -H "Content-Type: application/json" \ -d '{"value":"8443"}' http://127.0.0.1:8443/api/v1/settings/ini/RestApiPortCreating an account answers
{"address":"[email protected]","created":true}, or400for a missing field,404 {"error":"domain not found"}or409 {"error":"account already exists"}. In 6.3.4 the create route takesaddressandpasswordand the other columns of the CSV import as well (first_name,last_name,active,max_size_mbandadmin_level), and refuses a field it does not know by name (CommandLine.md). The older statement in the wiki and the APIs Reference, that onlyaddressandpasswordare read, no longer holds. For a log,linesdefaults to 200 and is capped at 2000.Limits and status codes
Limit Value What the client sees Request size 64 KB, head and body 413, connection closedLarge requests 16 MB and 300 s on POST /api/v1/me/messagesandPOST /api/v1/me/drafts; a few other routes are granted more than 64 KB as well, among themPUT /api/v1/me/settingsand the SAML ACS (256 KB)413above thatRequest deadline 30 s, the TLS handshake included Connection closed with no response Connection lifetime 300 s, and 1000 requests Connection closed Concurrent connections 64 Connection closed Rate 200 requests per 10 s per credential 429withRetry-After: 10Auto-ban Shared with SMTP, IMAP and POP3; loopback exempt The address is refused at accept for 5 minutes A
401is deliberately the same for no credential, a wrong password and an unknown, expired or out-of-network key; the application log says which. A request withTransfer-Encodingis answered411, because the server does not de-chunk. An API response isapplication/jsonwithCache-Control: no-store, with exceptions: the static pages and the attachment download serve their own media types, a report asked for withformat=csvistext/csv, and every answer under/scim/isapplication/scim+json.Where to read more
- The wiki: The REST API and, for a row per route, APIs Reference section 4.
- The operator documents: Roles.md and SettingsApply.md.
- The public documentation: chapter 24, The REST API, with API keys and the OpenAPI document.
- On this forum: The Control Deck, which is the browser front end for this API; the Linux guide; the 6.3.4 announcement; hmctl and the PowerShell module; the send API and webhooks; and SCIM provisioning and single sign-on.
- Read-only is decided by route kind, not by HTTP method. A read-only key on a mutating route is answered
-
C chris pinned this topic on
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login