---
title: "The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples"
url: https://www.hmailserver.co.uk/topic/28264/the-rest-api-turning-it-on-credentials-roles-one-time-codes-openapi-and-examples
author: "Progressiverobot"
published: 2026-09-27T19:46:13.731Z
updated: 2026-09-27T19:46:13.731Z
category: "REST API & integrations"
tags: ["rest-api","api","security","administration","tls"]
site: hMailServer Community Forum
---

# 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 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](/topic/28277). 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](/topic/28278).*



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](/topic/28222) 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. |

```bash
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.

```bash
# 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
```

```json
{"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](/topic/28237) 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:

```bash
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
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.

```bash
# 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](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/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.

```bash
# 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":"newuser@example.com","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/newuser@example.com

# 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":"newuser@example.com","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](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/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](https://gitlab.com/hmailserver/hmailserver/-/wikis/The-REST-API) and, for a row per route, [APIs Reference](https://gitlab.com/hmailserver/hmailserver/-/wikis/APIs-Reference) section 4.
- The operator documents: [Roles.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Roles.md) and [SettingsApply.md](https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingsApply.md).
- The public documentation: [chapter 24, The REST API](https://www.progressiverobot.com/hmailserver-documentation/#24-the-rest-api), with [API keys](https://www.progressiverobot.com/hmailserver-documentation/#2413-api-keys) and [the OpenAPI document](https://www.progressiverobot.com/hmailserver-documentation/#2417-the-openapi-document).
- On this forum: [The Control Deck](/topic/28192), which is the browser front end for this API; the [Linux guide](/topic/28189); the [6.3.4 announcement](/topic/28218); [hmctl and the PowerShell module](/topic/28265); [the send API and webhooks](/topic/28266); and [SCIM provisioning and single sign-on](/topic/28267).
