Skip to content
  • Categories
  • Recent
  • Popular
  • Tags
  • Users
  • Groups
  • Documentation
  • Downloads
  • Report a bug
  • GitHub mirror
  • Paid support
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
hMailServer Community Forum
hMailServer 6.3.8 is the current release, signed 2 November 2026. Windows and Linux, and an in-place upgrade from 5.3 or later, but for the original hMailServer's 5.7.Download 6.3.8 Documentation FAQ
  1. Support and discussion for hMailServer
  2. Automation & integration
  3. REST API & integrations
  4. The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples

The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples

Scheduled Pinned Locked Moved REST API & integrations
rest-apiapisecurityadministrationtls
1 Posts 1 Posters 133 Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • ProgressiverobotP
    ProgressiverobotP
    Progressiverobot
    hMailServer Team
    wrote on last edited by chris
    #1

    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 an Origin that is not this server's); scripts (the curl examples below among them), hmctl and 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, or serveradmin given with a list of domains) is refused a domain's rename, a domain alias, a custom role's creation, change or delete, the bind_to_address, send_using_route and script_function actions on a domain's or an account's rule, a relay host changed to an address that is not public, and GET /api/v1/metrics/history; and a role is given or taken away only by a credential that holds manage_roles in 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 as domains, allowed_from, role or expires on POST /api/v1/apikeys as 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 curl examples 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.4 RestApiPort is a setting stored in the database. Editing the [Settings] section of hMailServer.ini does 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
    RestApiPort 0 The port. 0 means there is no listener.
    RestApiBindAddress 127.0.0.1 The address it binds. TLS is required unless this is 127.0.0.1, localhost or ::1.
    RestApiCertificateFile empty The certificate, in PEM. Left empty, the listener falls back to the ACME certificate.
    RestApiPrivateKeyFile empty Its private key, in PEM.

    Since 6.3.4 the database is the settings store (schema 6042) and the [Settings] section of hMailServer.ini is 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 RestApiPort is set
    Windows The Control Panel page Monitoring & troubleshooting → API & monitoring, or Settings.SetIniSetting over COM. The Control Panel and the COM API are Windows-only.
    Linux hmailserver --set-setting on 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-setting checks what it is given: a port must be a number from 0 to 65535 and RestApiBindAddress an IP address or localhost.

    On Linux this matters most, because there the REST API is the administration interface. The packaged /etc/hmailserver/hMailServer.ini ships RestApiPort=0 and RestApiBindAddress=127.0.0.1 under [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 as HM5807 each 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, RestApiCertificateFile and RestApiPrivateKeyFile are among the 28 keys marked reinitialize, 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, at Application.Reinitialize over COM (Windows), at POST /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 by HM5113 naming the certificate or key file Correct the PEM paths and permissions
    Nothing RestApi: Listening on 127.0.0.1:8443 (http, loopback only). or ... (https). None needed

    The Listening on line is the only proof that the listener is up. The loopback exemption is a string comparison against 127.0.0.1, localhost and ::1; 127.0.0.2 and ::ffff:127.0.0.1 do 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: Basic with the user name Administrator (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: Basic with the address and the mailbox password or an app password, or the hmailsession cookie /api/v1/me and /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 administrator is 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 an expires (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, in hMailServerApiKeys.ini beside hMailServer.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"}
    

    key is the clear text. It is returned once and cannot be recovered, so store it then. GET /api/v1/apikeys lists the keys with an expired flag and never a hash; DELETE /api/v1/apikeys/<id> revokes one. The create request takes label (required, at most 64 characters), scope, domains, expires and allowed_from, and since 6.3.4 role and the send API's send_from, send_per_minute and send_per_day.

    What no API key reaches. Whatever its scope or role, a key is answered 401 (not 403, so that it learns nothing) on the key store /api/v1/apikeys and 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 credential POST /api/v1/send takes (a key naming a sender) and the only one /scim/v2 takes (a key whose role holds provision).

    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=full is serveradmin and Scope=readonly is auditor. A key is given a role with role on POST /api/v1/apikeys; an account is given one at /api/v1/role-assignments by a credential holding manage_roles.

    What a refusal says. A route a role may not call answers 403 naming the role and the permission, for example the role 'helpdesk' does not hold the permission 'manage_settings'. An address in a domain the role does not reach answers the role 'helpdesk' does not reach that domain whether 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/apikeys and /api/v1/settings/ini, that answer to the administrator password alone, not even to an account holding serveradmin.

    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/status
    

    Without it the answer is the one 401 on 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 401 and 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 with POST /api/v1/session and use the hmailsession cookie 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/session
    

    The 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_seconds and session_lifetime_seconds), at most 1000 sessions, and every session dropped when the listener stops. A cookie-authenticated request that is not GET or HEAD without X-Requested-With: hMailServer is refused with 403. 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 429 with code second_factor_locked and Retry-After. And for an account, one of its one-time recovery codes, in X-hMailServer-Recovery-Code, is taken wherever the authenticator's code is asked for.

    The administrator signs in at POST /api/v1/session with HTTP Basic, and with the code in X-hMailServer-OTP when 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/session among 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-Site or an Origin that 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 403 with this 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 403 with this 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 is 404.

    The OpenAPI document

    GET /api/v1/openapi.json answers 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-applies mark 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/status mentions uptime, and the handler returns no uptime field;
    • the text for /api/v1/settings/ini still 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 reinitialize or restart whose 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 empty
    GET /api/v1/settings/ini The keys as {name, value, secret}, an applies map and a pending array of names
    GET, PUT, DELETE /api/v1/settings/ini/{name} One key, with applies and pending
    POST /api/v1/server/reinitialize 202 before it happens; {"reinitializing": true, "drained": true} when it ended a drain

    All but the last answer to the administrator password alone. A DELETE is 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/status until it answers again.

    Worked examples

    These use the loopback, plain-HTTP shape that RestApiPort=8443 and RestApiBindAddress=127.0.0.1 give; put the port that was stored in place of 8443. With a certificate configured, change http:// to https://. 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/RestApiPort
    

    Creating an account answers {"address":"[email protected]","created":true}, or 400 for a missing field, 404 {"error":"domain not found"} or 409 {"error":"account already exists"}. In 6.3.4 the create route takes address and password and the other columns of the CSV import as well (first_name, last_name, active, max_size_mb and admin_level), and refuses a field it does not know by name (CommandLine.md). The older statement in the wiki and the APIs Reference, that only address and password are read, no longer holds. For a log, lines defaults 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 closed
    Large requests 16 MB and 300 s on POST /api/v1/me/messages and POST /api/v1/me/drafts; a few other routes are granted more than 64 KB as well, among them PUT /api/v1/me/settings and the SAML ACS (256 KB) 413 above that
    Request 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 429 with Retry-After: 10
    Auto-ban Shared with SMTP, IMAP and POP3; loopback exempt The address is refused at accept for 5 minutes

    A 401 is 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 with Transfer-Encoding is answered 411, because the server does not de-chunk. An API response is application/json with Cache-Control: no-store, with exceptions: the static pages and the attachment download serve their own media types, a report asked for with format=csv is text/csv, and every answer under /scim/ is application/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.
    1 Reply Last reply
    0
    • chrisC 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
    Reply
    • Reply as topic
    Log in to reply
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes


    hMailServer Community

    The community support forum for hMailServer — the free, open-source mail server for Windows and Linux — maintained by Progressive Robot Ltd. The software is AGPL-3.0 and stays that way; this is where it is discussed, debugged and documented.

    The software

    • Documentation
    • Downloads
    • hMailServer 6.3
    • Source on GitLab

    Getting help

    • Forum rules
    • About this forum
    • Support plans
    • Pay monthly support
    • Talk to Progressive Robot

    Legal

    • Privacy policy
    • Terms and conditions
    • Cookie policy

    Progressive Robot Ltd · 220 Chester Business Park, Heronsway, Chester, Cheshire CH4 9GB, United Kingdom · +44 (0)1244 911212 · [email protected] · VAT 506152326

    Powered by NodeBB

    • Login

    • Don't have an account? Register

    • Login or register to search.
    • First post
      Last post
    0
    • Categories
    • Recent
    • Popular
    • Tags
    • Users
    • Groups
    • Documentation
    • Downloads
    • Report a bug
    • GitHub mirror
    • Paid support