<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The REST API: turning it on, credentials, roles, one-time codes, OpenAPI and examples]]></title><description><![CDATA[<p dir="auto"><em>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 (<code>Sec-Fetch-Site</code>, or an <code>Origin</code> that is not this server's); scripts (the <code>curl</code> examples below among them), <code>hmctl</code> 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 <code>serveradmin</code> given with a list of domains) is refused a domain's rename, a domain alias, a custom role's creation, change or delete, the <code>bind_to_address</code>, <code>send_using_route</code> and <code>script_function</code> actions on a domain's or an account's rule, a relay host changed to an address that is not public, and <code>GET /api/v1/metrics/history</code>; and a role is given or taken away only by a credential that holds <code>manage_roles</code> 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 &amp; monitoring is Monitoring &amp; alerts and REST API keys is under Sign-in &amp; access; Ctrl+K takes every old title to its page. See <a href="/topic/28277">the 6.3.7 announcement</a>. 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 <code>domains</code>, <code>allowed_from</code>, <code>role</code> or <code>expires</code> on <code>POST /api/v1/apikeys</code> 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, <code>AccountLockoutThreshold</code> (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 <a href="/topic/28278">the 6.3.8 announcement</a>.</em></p>
<p dir="auto">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 <code>curl</code> examples that use only routes the project's own documents name. <strong>The one thing to know first:</strong> the listener is off as shipped (<code>RestApiPort=0</code>), and since 6.3.4 <code>RestApiPort</code> is a setting stored in the database. Editing the <code>[Settings]</code> section of <code>hMailServer.ini</code> does not turn the API on once the server has stored the key.</p>
<h3><a class="anchor-offset" name="turning-the-listener-on"></a>Turning the listener on</h3>
<p dir="auto">Four settings shape the listener.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Setting</th>
<th>Shipped value</th>
<th>What it is</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>RestApiPort</code></td>
<td><code>0</code></td>
<td>The port. <code>0</code> means there is no listener.</td>
</tr>
<tr>
<td><code>RestApiBindAddress</code></td>
<td><code>127.0.0.1</code></td>
<td>The address it binds. TLS is required unless this is <code>127.0.0.1</code>, <code>localhost</code> or <code>::1</code>.</td>
</tr>
<tr>
<td><code>RestApiCertificateFile</code></td>
<td>empty</td>
<td>The certificate, in PEM. Left empty, the listener falls back to the ACME certificate.</td>
</tr>
<tr>
<td><code>RestApiPrivateKeyFile</code></td>
<td>empty</td>
<td>Its private key, in PEM.</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>Since 6.3.4 the database is the settings store</strong> (schema 6042) and the <code>[Settings]</code> section of <code>hMailServer.ini</code> 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. <a href="/topic/28222">Settings: what is not stock, when a save takes effect</a> covers the store itself, why an edited line is put back (<code>HM5804</code>) and <code>[SettingsOverride]</code>. Where the stored value is changed depends on the platform.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Platform</th>
<th>How <code>RestApiPort</code> is set</th>
</tr>
</thead>
<tbody>
<tr>
<td>Windows</td>
<td>The Control Panel page <strong>Monitoring &amp; troubleshooting → API &amp; monitoring</strong>, or <code>Settings.SetIniSetting</code> over COM. The Control Panel and the COM API are Windows-only.</td>
</tr>
<tr>
<td>Linux</td>
<td><code>hmailserver --set-setting</code> on the server itself, then a restart of the service.</td>
</tr>
<tr>
<td>Either, once the API answers</td>
<td><code>PUT /api/v1/settings/ini/{name}</code> with the administrator password.</td>
</tr>
</tbody>
</table>
<pre><code class="language-bash">sudo hmailserver --set-setting RestApiPort=8443
sudo systemctl restart hmailserver
</code></pre>
<p dir="auto"><code>--set-setting</code> checks what it is given: a port must be a number from 0 to 65535 and <code>RestApiBindAddress</code> an IP address or <code>localhost</code>.</p>
<p dir="auto"><strong>On Linux this matters most</strong>, because there the REST API is the administration interface. The packaged <code>/etc/hmailserver/hMailServer.ini</code> ships <code>RestApiPort=0</code> and <code>RestApiBindAddress=127.0.0.1</code> under <code>[Settings]</code>, 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 <code>[SettingsOverride]</code> section, applied over the store at every start and reported as <code>HM5807</code> each time; the wiki gives it as the route where neither the Control Panel nor COM is available.</p>
<p dir="auto"><strong>The new value waits for a reinitialise.</strong> Since 6.3.4 most settings apply when they are saved, but <code>RestApiPort</code>, <code>RestApiBindAddress</code>, <code>RestApiCertificateFile</code> and <code>RestApiPrivateKeyFile</code> are among the 28 keys marked <code>reinitialize</code>, because the listener binds and its TLS context is built when the servers start. They take effect at <strong>Apply now</strong> in the Control Panel (Windows) or the Control Deck, at <code>Application.Reinitialize</code> over COM (Windows), at <code>POST /api/v1/server/reinitialize</code>, or at a restart of the service.</p>
<p dir="auto"><strong>The listener can refuse to start</strong>, and each refusal is written once to the application log.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>What is wrong</th>
<th>Log line</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>The administrator password is empty</td>
<td><code>RestApi: Refusing to start - the administrator password is not set.</code></td>
<td>Set an administrator password</td>
</tr>
<tr>
<td>The bind address is not loopback and there is no certificate</td>
<td><code>RestApi: Refusing to start - TLS certificate is required unless bound to 127.0.0.1 or ::1. Set RestApiCertificateFile and RestApiPrivateKeyFile.</code></td>
<td>Set both files, enable ACME, or bind loopback</td>
</tr>
<tr>
<td>The certificate cannot be loaded, or its key does not match</td>
<td><code>RestApi: Refusing to start - the shared TLS configuration could not be applied to the configured certificate.</code>, preceded by <code>HM5113</code> naming the certificate or key file</td>
<td>Correct the PEM paths and permissions</td>
</tr>
<tr>
<td>Nothing</td>
<td><code>RestApi: Listening on 127.0.0.1:8443 (http, loopback only).</code> or <code>... (https).</code></td>
<td>None needed</td>
</tr>
</tbody>
</table>
<p dir="auto">The <code>Listening on</code> line is the only proof that the listener is up. The loopback exemption is a string comparison against <code>127.0.0.1</code>, <code>localhost</code> and <code>::1</code>; <code>127.0.0.2</code> and <code>::ffff:127.0.0.1</code> do not qualify. The TLS configuration is the one the mail protocols use (cipher list, protocol switches, <code>TlsKeyExchangeGroups</code>), 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.</p>
<h3><a class="anchor-offset" name="the-three-credentials"></a>The three credentials</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Credential</th>
<th>Header</th>
<th>Reaches</th>
</tr>
</thead>
<tbody>
<tr>
<td>Administrator password</td>
<td><code>Authorization: Basic</code> with the user name <code>Administrator</code> (case-insensitive)</td>
<td>Every administrative route. It is never read-only and never confined to domains.</td>
</tr>
<tr>
<td>API key</td>
<td><code>Authorization: Bearer hmapi_&lt;64 hex&gt;</code></td>
<td>What its scope, role and domains allow, less the routes listed below.</td>
</tr>
<tr>
<td>An account's own</td>
<td><code>Authorization: Basic</code> with the address and the mailbox password or an app password, or the <code>hmailsession</code> cookie</td>
<td><code>/api/v1/me</code> and <code>/api/v1/session</code>; since 6.3.4 also the administrative routes a role it holds allows.</td>
</tr>
</tbody>
</table>
<p dir="auto">The two Basic forms are told apart before either password is tried: any user name but <code>administrator</code> 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.</p>
<p dir="auto"><strong>API keys are the credential for scripts.</strong> A key is read-only unless it is made with <code>"scope":"full"</code>, expires after 90 days unless the request names an <code>expires</code> (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 <code>hMailServerApiKeys.ini</code> beside <code>hMailServer.ini</code>, and that file is read again at every authentication attempt, so removing a key's section revokes it on the next request.</p>
<pre><code class="language-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
</code></pre>
<pre><code class="language-json">{"id":"3f9a1c4b5d6e7f80","label":"backup monitor","scope":"readonly","domains":"",
 "expires":"2027-01-01 00:00:00","allowed_from":"","key":"hmapi_7c1f...c92a"}
</code></pre>
<p dir="auto"><code>key</code> is the clear text. It is returned once and cannot be recovered, so store it then. <code>GET /api/v1/apikeys</code> lists the keys with an <code>expired</code> flag and never a hash; <code>DELETE /api/v1/apikeys/&lt;id&gt;</code> revokes one. The create request takes <code>label</code> (required, at most 64 characters), <code>scope</code>, <code>domains</code>, <code>expires</code> and <code>allowed_from</code>, and since 6.3.4 <code>role</code> and the send API's <code>send_from</code>, <code>send_per_minute</code> and <code>send_per_day</code>.</p>
<p dir="auto"><strong>What no API key reaches.</strong> Whatever its scope or role, a key is answered <code>401</code> (not <code>403</code>, so that it learns nothing) on the key store <code>/api/v1/apikeys</code> and on <code>/api/v1/settings/ini</code>, and since 6.3.4 also on <code>/api/v1/audit</code>, <code>/api/v1/alerts</code>, <code>/api/v1/webhooks</code>, the compromised-account review, <code>/api/v1/settings/changed</code>, <code>/api/v1/server/settings/pending</code>, 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 <code>POST /api/v1/send</code> takes (a key naming a sender) and the only one <code>/scim/v2</code> takes (a key whose role holds <code>provision</code>).</p>
<h3><a class="anchor-offset" name="roles-since-6.3.4"></a>Roles, since 6.3.4</h3>
<p dir="auto">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: <code>Scope=full</code> is <code>serveradmin</code> and <code>Scope=readonly</code> is <code>auditor</code>. A key is given a role with <code>role</code> on <code>POST /api/v1/apikeys</code>; an account is given one at <code>/api/v1/role-assignments</code> by a credential holding <code>manage_roles</code>.</p>
<p dir="auto"><strong>What a refusal says.</strong> A route a role may not call answers <code>403</code> naming the role and the permission, for example <code>the role 'helpdesk' does not hold the permission 'manage_settings'</code>. An address in a domain the role does not reach answers <code>the role 'helpdesk' does not reach that domain</code> whether or not the account exists. A request authenticated with an app password never carries the account's role.</p>
<p dir="auto"><a href="/topic/28237">Administrator roles end to end</a> has the built-in roles and their permissions, giving a role on each surface, custom roles, and the two stores, <code>/api/v1/apikeys</code> and <code>/api/v1/settings/ini</code>, that answer to the administrator password alone, not even to an account holding <code>serveradmin</code>.</p>
<h3><a class="anchor-offset" name="one-time-codes"></a>One-time codes</h3>
<p dir="auto">When a second factor is enrolled on the administrator credential, the code travels in an extra header:</p>
<pre><code class="language-bash">curl -u Administrator:your-admin-password \
     -H "X-hMailServer-OTP: 123456" \
     http://127.0.0.1:8443/api/v1/status
</code></pre>
<p dir="auto">Without it the answer is the one <code>401</code> on this listener that says what is missing:</p>
<pre><code class="language-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"}
</code></pre>
<p dir="auto">A missing code is not counted towards the auto-ban; a wrong code is.</p>
<p dir="auto"><strong>Since 6.3.4 a one-time code is taken once</strong>, 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 <code>401</code> 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 <code>POST /api/v1/session</code> and use the <code>hmailsession</code> cookie from there, or use an API key.</p>
<pre><code class="language-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
</code></pre>
<p dir="auto">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, <code>session_idle_seconds</code> and <code>session_lifetime_seconds</code>), at most 1000 sessions, and every session dropped when the listener stops. A cookie-authenticated request that is not <code>GET</code> or <code>HEAD</code> without <code>X-Requested-With: hMailServer</code> is refused with <code>403</code>. 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.</p>
<p dir="auto">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 <code>429</code> with <code>code</code> <code>second_factor_locked</code> and <code>Retry-After</code>. And for an account, one of its one-time recovery codes, in <code>X-hMailServer-Recovery-Code</code>, is taken wherever the authenticator's code is asked for.</p>
<p dir="auto">The administrator signs in at <code>POST /api/v1/session</code> with HTTP Basic, and with the code in <code>X-hMailServer-OTP</code> when a second factor is enrolled: <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/AccountRecovery.md" rel="nofollow ugc">AccountRecovery.md</a> 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 <code>/api/v1/session</code> among the routes the administrator password does not reach, is out of date on this.</p>
<h3><a class="anchor-offset" name="how-a-request-is-judged"></a>How a request is judged</h3>
<p dir="auto">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 <code>Sec-Fetch-Site</code> or an <code>Origin</code> 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.</p>
<ul>
<li><strong>Read-only is decided by route kind, not by HTTP method.</strong> A read-only key on a mutating route is answered <code>403</code> with <code>this api key is read-only</code>.</li>
<li><strong>A key with a domain list is refused server-wide resources outright</strong>: 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 <code>403</code> with <code>this api key is not permitted for that domain</code>.</li>
<li><strong>Routing is exact.</strong> 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 <code>/api/v1/me/...</code> path must be 1 to 18 decimal digits and greater than zero, or the answer is <code>404</code>.</li>
</ul>
<h3><a class="anchor-offset" name="the-openapi-document"></a>The OpenAPI document</h3>
<p dir="auto"><code>GET /api/v1/openapi.json</code> 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 <strong>441 paths and 621 path-and-method operations</strong>, 413 and 585 of them under <code>/api/</code>, against 146 and 219 in 6.3.3. The rest are <code>/scim/v2</code>, the sign-in, reset and reading pages, the list manager's pages and the static catalogues and scripts.</p>
<p dir="auto">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 <code>x-applies</code> 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:</p>
<ul>
<li>the summary of <code>/api/v1/status</code> mentions uptime, and the handler returns no uptime field;</li>
<li>the text for <code>/api/v1/settings/ini</code> still describes the file as the store, where since 6.3.4 the value goes to the database first.</li>
</ul>
<h3><a class="anchor-offset" name="settings-saved-and-not-yet-running"></a>Settings saved and not yet running</h3>
<p dir="auto">Since 6.3.4 a key marked <code>reinitialize</code> or <code>restart</code> whose stored value is not the value the server is running on is <strong>pending</strong>. The list is recomputed at every request.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Route</th>
<th>Answer</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET /api/v1/server/settings/pending</code></td>
<td><code>[{name, stored, running, applies, secret}]</code>; a secret's two values are <code>(set)</code> or empty</td>
</tr>
<tr>
<td><code>GET /api/v1/settings/ini</code></td>
<td>The keys as <code>{name, value, secret}</code>, an <code>applies</code> map and a <code>pending</code> array of names</td>
</tr>
<tr>
<td><code>GET</code>, <code>PUT</code>, <code>DELETE /api/v1/settings/ini/{name}</code></td>
<td>One key, with <code>applies</code> and <code>pending</code></td>
</tr>
<tr>
<td><code>POST /api/v1/server/reinitialize</code></td>
<td><code>202</code> before it happens; <code>{"reinitializing": true, "drained": true}</code> when it ended a drain</td>
</tr>
</tbody>
</table>
<p dir="auto">All but the last answer to the administrator password alone. A <code>DELETE</code> 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 <code>/api/v1/status</code> until it answers again.</p>
<h3><a class="anchor-offset" name="worked-examples"></a>Worked examples</h3>
<p dir="auto">These use the loopback, plain-HTTP shape that <code>RestApiPort=8443</code> and <code>RestApiBindAddress=127.0.0.1</code> give; put the port that was stored in place of 8443. With a certificate configured, change <code>http://</code> to <code>https://</code>. 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.</p>
<pre><code class="language-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
</code></pre>
<p dir="auto">Creating an account answers <code>{"address":"newuser@example.com","created":true}</code>, or <code>400</code> for a missing field, <code>404 {"error":"domain not found"}</code> or <code>409 {"error":"account already exists"}</code>. In 6.3.4 the create route takes <code>address</code> and <code>password</code> and the other columns of the CSV import as well (<code>first_name</code>, <code>last_name</code>, <code>active</code>, <code>max_size_mb</code> and <code>admin_level</code>), and refuses a field it does not know by name (<a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/CommandLine.md" rel="nofollow ugc">CommandLine.md</a>). The older statement in the wiki and the APIs Reference, that only <code>address</code> and <code>password</code> are read, no longer holds. For a log, <code>lines</code> defaults to 200 and is capped at 2000.</p>
<h3><a class="anchor-offset" name="limits-and-status-codes"></a>Limits and status codes</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Limit</th>
<th>Value</th>
<th>What the client sees</th>
</tr>
</thead>
<tbody>
<tr>
<td>Request size</td>
<td>64 KB, head and body</td>
<td><code>413</code>, connection closed</td>
</tr>
<tr>
<td>Large requests</td>
<td>16 MB and 300 s on <code>POST /api/v1/me/messages</code> and <code>POST /api/v1/me/drafts</code>; a few other routes are granted more than 64 KB as well, among them <code>PUT /api/v1/me/settings</code> and the SAML ACS (256 KB)</td>
<td><code>413</code> above that</td>
</tr>
<tr>
<td>Request deadline</td>
<td>30 s, the TLS handshake included</td>
<td>Connection closed with no response</td>
</tr>
<tr>
<td>Connection lifetime</td>
<td>300 s, and 1000 requests</td>
<td>Connection closed</td>
</tr>
<tr>
<td>Concurrent connections</td>
<td>64</td>
<td>Connection closed</td>
</tr>
<tr>
<td>Rate</td>
<td>200 requests per 10 s per credential</td>
<td><code>429</code> with <code>Retry-After: 10</code></td>
</tr>
<tr>
<td>Auto-ban</td>
<td>Shared with SMTP, IMAP and POP3; loopback exempt</td>
<td>The address is refused at accept for 5 minutes</td>
</tr>
</tbody>
</table>
<p dir="auto">A <code>401</code> 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 <code>Transfer-Encoding</code> is answered <code>411</code>, because the server does not de-chunk. An API response is <code>application/json</code> with <code>Cache-Control: no-store</code>, with exceptions: the static pages and the attachment download serve their own media types, a report asked for with <code>format=csv</code> is <code>text/csv</code>, and every answer under <code>/scim/</code> is <code>application/scim+json</code>.</p>
<h3><a class="anchor-offset" name="where-to-read-more"></a>Where to read more</h3>
<ul>
<li>The wiki: <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/The-REST-API" rel="nofollow ugc">The REST API</a> and, for a row per route, <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/APIs-Reference" rel="nofollow ugc">APIs Reference</a> section 4.</li>
<li>The operator documents: <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Roles.md" rel="nofollow ugc">Roles.md</a> and <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/SettingsApply.md" rel="nofollow ugc">SettingsApply.md</a>.</li>
<li>The public documentation: <a href="https://www.progressiverobot.com/hmailserver-documentation/#24-the-rest-api" rel="nofollow ugc">chapter 24, The REST API</a>, with <a href="https://www.progressiverobot.com/hmailserver-documentation/#2413-api-keys" rel="nofollow ugc">API keys</a> and <a href="https://www.progressiverobot.com/hmailserver-documentation/#2417-the-openapi-document" rel="nofollow ugc">the OpenAPI document</a>.</li>
<li>On this forum: <a href="/topic/28192">The Control Deck</a>, which is the browser front end for this API; the <a href="/topic/28189">Linux guide</a>; the <a href="/topic/28218">6.3.4 announcement</a>; <a href="/topic/28265">hmctl and the PowerShell module</a>; <a href="/topic/28266">the send API and webhooks</a>; and <a href="/topic/28267">SCIM provisioning and single sign-on</a>.</li>
</ul>
]]></description><link>https://www.hmailserver.co.uk/topic/28264/the-rest-api-turning-it-on-credentials-roles-one-time-codes-openapi-and-examples</link><generator>RSS for Node</generator><lastBuildDate>Fri, 06 Nov 2026 09:05:49 GMT</lastBuildDate><atom:link href="https://www.hmailserver.co.uk/topic/28264.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 27 Sep 2026 19:46:13 GMT</pubDate><ttl>60</ttl></channel></rss>