Move a licence to a different machine
When to use: a licence key stops matching the server it is installed on. The admin UI shows a red banner and Licence reads degraded with “This licence key is registered to a different machine”; mailctl license status says the same and exits 1. This normally follows a restore onto new hardware, or a rebuilt host restored from a backup. The machine ID hashes the host’s machine-id, the MAC of the network interface carrying the default route (never a Docker or other virtual interface) and the hostname, but since 0.4.1 it is computed by mailctl and written to .env as MACHINE_FINGERPRINT at install, upgrade and restore, and by this command when it differs; the running server reads that value, so a hostname change or a replaced NIC does not degrade it.
A changed host shows up at the next mailctl upgrade instead, refused with “registered to a different machine” and a line naming the machine ID .env recorded and this host’s. Run mailctl license transfer below, then the upgrade again (Upgrade step 4). After asking for credentials, the command writes this host’s ID to .env and restarts the api and worker on it, which is what lets the server see the mismatch. It then reads degraded until the move succeeds. Mail is unaffected. That first step is from 0.4.2. With mailctl 0.4.1 this command reported nothing to transfer, and the answer was support.
Preconditions: owner access to this server — either the admin UI signed in as the owner, or a shell on the host and the owner’s email and password (typed into a real terminal: activation and transfer are interactive by decision, see From the host below). Nobody else needs to be involved: the licence server accepts the key already installed here as proof of the move. The manual procedure below is the fallback for the cases that cannot prove themselves.
Nothing is broken while you do this. A degraded server refuses new mailboxes and domains, AI features, starting a migration and new sending tokens, and nothing else: delivery, IMAP, POP3, webmail, sending, authentication and the rest of the admin UI all continue. A key on another machine is a technical lapse, so it stays
degradedhowever long it takes and never reaches the stages that make administration read-only or stop sending. There is no hurry, and no reason to take mail down for it.
From the admin UI: Licence → This server shows both machine IDs and, for the owner, a Move this licence to this server button. Click it. The page reports the new state in place — the banner goes, and Key is bound to now matches Machine ID. That is the whole procedure. Not after a refused mailctl upgrade on a changed host: the api still runs on the old machine ID, so the page shows no mismatch and no button. Use mailctl license transfer on the host, which records the new ID first.
From the host:
mailctl license transferIt asks for the owner’s credentials (or reads --token/MAILDOCK_TOKEN) first. Either is checked before anything changes: a sign-in by the api, and from 0.11.0 a token too — it must be accepted and carry the owner role (GET /auth/principal), or the command stops with Nothing was changed. Up to 0.10.0 a token was taken as given and a wrong one was found out only after the restart, leaving the server degraded until the command was run again with a valid one. Then, on the install’s own host, when this host’s machine ID differs from the one in .env, it records the new one and restarts api and worker on it (not with --api-url). From 0.4.4; 0.4.2 and 0.4.3 restarted first, so a run with no credentials left the server degraded until the move was finished. Run it from a real terminal: a shell with no TTY cannot answer the sign-in prompt. Activation and transfer are interactive by decision (2026-10-10): the tokens page offers no owner token, and --token remains only for the rare host with no terminal. Then it prints the diagnosis (licence id, the machine the key is bound to, this machine), asks the licence server to move the licence, installs the re-issued key and prints the new status. It also records LICENSE_ID in .env, so the next mailctl upgrade can still authenticate to the image registry.
A licence that is portable, already bound here, or absent is reported as “nothing to transfer” instead — stop, and look for a different reason for the degrade in mailctl license status.
Except a portable key another server has claimed. The licence server binds a portable key’s licence to the first machine that checks in with it. Any other server using that key is then refused on every heartbeat with fingerprint_mismatch. mailctl license status and the Licence page say Another server has claimed this licence, and transfer refuses: the licence server only moves a licence for the key that names its current machine. Nothing on this server can move it back, and that server’s key cannot be installed here: it names that server, so activate refuses it. The way out is support moving the licence and issuing a key for this machine, installed with mailctl license transfer --key <key>. If both servers should run, each needs its own licence.
When it refuses
Section titled “When it refuses”Each of these ends somewhere different. The codes and quoted texts below are the API’s (POST /license/transfer); mailctl license transfer prints neither. It prints a sentence of its own for license_server_unconfigured, license_transfer_stale and license_revoked, and “The licence server could not be asked:” followed by the API’s message for everything else, then the manual steps, and exits 1 (manualFallback, apps/cli/src/lib/license.ts). license_not_installed never reaches the command: with no key installed it stops earlier with “No licence is installed on this server” and asks nothing.
| What you see | What it means | What to do |
|---|---|---|
license_server_unconfigured — “LICENSE_SERVER_URL is not set” |
This install was never pointed at a licence server. | Set LICENSE_SERVER_URL in the install’s .env and recreate the containers that read it (docker compose … up -d api worker — a restart keeps the old value), or use By hand below. |
license_server_unreachable |
Network, DNS, or the licence server is down. | Retry; a licence server may be down for days without a customer noticing. Or use By hand. |
license_transfer_stale — “already moved away from” |
The licence has since been transferred to another machine, so this server’s key cannot move it. | Ask support to move it here: they issue a key for this machine, installed with mailctl license transfer --key. Nothing on this server can move it back. |
license_revoked |
The licence was revoked deliberately (fraud, a chargeback). | Ask support. Nothing on this host will change that. |
license_not_installed |
No verified licence key is installed on this server, so there is nothing to transfer. | Install the key first (mailctl license activate), or run the transfer from the machine that holds it. |
license_transfer_rejected — “did not recognise this key” |
The licence server does not recognise the key: it was issued by a different licence server or signing key. | Check LICENSE_SERVER_URL points at the server that issued the key; otherwise ask support. |
license_unknown |
The licence server has no record of this licence. | Ask support, with the licence id from mailctl license status. |
By hand
Section titled “By hand”For a host that cannot present a working key at all — the key is gone, or the licence has moved on without it. Somebody with the licence server’s admin bearer token performs step 2.
-
On the new host, read this server’s machine ID:
mailctl license statusprints it as Machine ID, and Licence → This server in the admin UI shows the same. With no key installed, neither can tell you which machine the licence was bound to —mailctl license transferonly reports that there is nothing to transfer — so if you need the old ID, it comes from support or the licence server’s records. -
Rebind the licence:
curl -sS -X POST https://<license-server>/v1/admin/licenses/<licence-id>/transfer \-H "authorization: Bearer $LICENSE_ADMIN_TOKEN" \-H 'content-type: application/json' \-d '{"fingerprint":"<new machine id>"}'The response carries the re-issued
key. Nothing else about the subscription changes — same tier, same expiry, same licence id. -
Install the re-issued key on the new host:
mailctl license transfer --key mdl1.…The owner can do the same from Licence → Replace the key in the admin UI.
-
Decommission the old host if it still exists. It keeps running on its old key — which still names its machine, so it does not degrade on a fingerprint. What changes is its heartbeat: the licence server now answers it with
409 fingerprint_mismatch, its check-in clock stops advancing, and it enters grace after 7 days, degrades 30 days and becomes read-only (restricted) 60 days after its last successful check-in. A stale heartbeat is a technical lapse and goes no further, so mail keeps flowing on it throughout.
Verification
Section titled “Verification”mailctl license status exits 0 and reports active, with Key bound to equal to Machine ID. The admin UI’s banner is gone. Last check-in reads not yet: installing a key deliberately restarts the heartbeat clock, because the old one described a different machine. The worker’s daily heartbeat fills it in within 24 hours; a server showing “not yet” is active, not degraded.
Rollback
Section titled “Rollback”Run the transfer again from the old host, which will now hold the stale key — so it will be refused with license_transfer_stale, and the by-hand path with the old machine ID is what actually reverses it. A transfer moves a binding and never invalidates the subscription, so nothing is lost by transferring in the wrong direction first.
Limits
Section titled “Limits”- Only the machine holding the current key can move a licence, which is what stops a decommissioned host taking it back after a legitimate move. It is also why the by-hand path still exists.
- A transfer is not rate-limited on the licence server. Nothing stops a key being moved repeatedly, which is a deliberate trade: a fingerprint is not a secret and bypass is accepted (a point left standing) — the update channel, not the running server, is where distribution is actually gated.
- Possession of this server’s key file is now enough to move the licence away from it. Recovery is symmetric — move it back — and the licence server records the trade in its own decision record.