Admin API

The administrative REST API: how to reach it, authentication, every endpoint with its method, path and purpose, answer shapes and errors.

The administrative API is the only way anything on Nixt Server is administered. versealx-server admin is a client of it; so is any script you write. Every change goes through one path that checks the caller’s role, makes the change and writes the audit line in the same transaction, so a change cannot happen without its record.

Reaching the API

Way inAddressWho you are
The local socketThe Unix socket at [admin] socket, /var/lib/versealx-server/admin.sock on a node set up with initThe operator, with no token. The socket is created with mode 0600 for the service user, so only that user — or root — can open it.
HTTPShttps://mail.example.com/api/v1/…, only when [listeners.admin] is setWhoever the bearer token says.

Both answer the same API. Paths begin with /api/v1.

The operator’s console

The console’s System pages — the cluster, disaster recovery and links — are the operator’s, and the operator is reached only from inside the server. On the server, as the service user, open the console on this machine:

sudo -u versealx versealx-server console serve --from /srv/versealx-console

--from is where a console release is installed (versealx-server console install). The command prints an address like http://127.0.0.1:43127/#operator=…. Open it in a browser on the server, or from your own machine through an SSH tunnel to that port:

ssh -L 43127:127.0.0.1:43127 admin@mail.example.com

In a cloud, your provider’s session manager port forwarding does the same. The console listens on the loopback address only and passes its calls to the local socket. The key in the address works until you stop the command with Ctrl-C; without it, nothing is answered. --port picks the port.

With versealx-server admin

sudo -u versealx versealx-server admin get tenants --config /etc/versealx-server/versealx-server.toml
PartMeaning
get, post, put, patch or deleteThe HTTP method, in any case.
The pathWith or without /api/v1/: tenants/1/domains and /api/v1/tenants/1/domains are the same.
key=value pairsFor get and delete, query parameters. For the other methods, fields of a JSON body.
--socket <path>A socket other than the one in the configuration.
--config <file>Where to find the configuration, and so the socket. A missing or unreadable file is not fatal; the default socket is used.

Values in a body are typed: true and false become booleans, null becomes null, a value made only of digits becomes a number, and a value starting with [ or { that parses as JSON is sent as that JSON. Everything else is a string. Quote pairs that contain spaces or shell characters.

The command prints the answer’s JSON, indented, and exits 0 for a success, 1 when the API answered with an error (4xx or 5xx), and 2 when the call could not be made at all.

MessageCause
cannot reach the admin socket at <path>: <reason> followed by is the server running, and does this account own it?The server is not running, the admin role is off, or you are not the service user.
the admin socket did not answerNo answer within 30 seconds.
The usage textThe method or path is missing, or the method is not one of the five.

With curl over the socket

sudo -u versealx curl --unix-socket /var/lib/versealx-server/admin.sock http://localhost/api/v1/tenants

Over HTTPS

[listeners.admin]
bind = "0.0.0.0:443"

With this table, the API answers on the shared HTTPS listener beside JMAP. Requests need an Authorization: Bearer <token> header carrying a token issued by this server for the versealx-admin audience; the token’s scopes name the caller’s role. See Roles and the audit log.

For a script or an identity provider — anything that cannot sit at the terminal and cannot open the socket — mint a provisioning token. It goes in the same Authorization: Bearer header, and the API treats it exactly as it treats any other credential carrying those scopes.

Requests and answers

  • Request bodies are JSON, at most 256 KiB.
  • Successful answers are JSON: 200 with the thing, 201 with a newly created thing, or 204 with no body.
  • Lists are wrapped as {"items": [...]}.
  • Times are Unix milliseconds unless a field says otherwise.
  • Reads are not audited unless refused; every change, successful, failed or refused, is.

The OpenAPI document

vsx admin get openapi.json

GET /api/v1/openapi.json returns an OpenAPI 3.1 description of every endpoint, including the Settings schema that lists every runtime setting. It needs no credentials. Endpoints only the operator may call are marked x-operator-only.

Endpoints

Provisioning tokens

The credential a machine authenticates with. See Provisioning with SCIM.

MethodPathSuccessPurpose
GET/tenants/{tenant}/provisioning-tokens200The tenant’s tokens: label, scope, tail (the last four characters of the secret), createdAt, expiresAt, lastUsedAt and expired. Never a secret.
POST/tenants/{tenant}/provisioning-tokens201Mint one. Body: label, scope (administrative scopes, space separated), days (1 to 365, 90 by default). The answer carries secret, once and only here. You cannot grant a scope you do not hold.
DELETE/tenants/{tenant}/provisioning-tokens/{id}204Revoke one. It stops working immediately.
GET/tenants/{tenant}/classifier200What the classifier has learnt: mode, learntSpam, learntNotSpam, the minimum of each it needs, and whether it is scoring.
DELETE/tenants/{tenant}/classifier204Forget everything the classifier has learnt.
GET/tenants/{tenant}/rules200The filter’s rules, in the order they run, with version, updatedAt and by. Tenant 0’s are the server’s, which run first.
PUT/tenants/{tenant}/rules200Replace the rules: rules, and the version you read. 409 if it has changed since; 400 naming the rule if the list cannot run. See Rules.
GET/tenants/{tenant}/rules/history200Every version of the rules, newest first.
GET/tenants/{tenant}/quarantine200Quarantined mail: account, address, message, size, receivedAt, quarantinedAt, sender, reason and queueId for each, with more when there were others. account=<id> for one account, paged with after; limit up to 1000. Never the subject or the message. A domain administrator sees their own domains.
POST/tenants/{tenant}/quarantine/{account}/{message}/release200Move a message into the account’s inbox as new mail.
DELETE/tenants/{tenant}/quarantine/{account}/{message}204Delete a quarantined message. Not available to helpdesk.

Tenants

MethodPathSuccessPurpose
GET/tenants200Every tenant. Operator only.
POST/tenants201Make a tenant. Body: name (1 to 64 characters, unique whatever its case). Operator only.
GET/tenants/{tenant}200The tenant: id, name, suspended, created_at.
PATCH/tenants/{tenant}200Suspend the tenant or bring it back; the operator only. Body: suspended (boolean).

Domains

MethodPathSuccessPurpose
GET/tenants/{tenant}/domains200The tenant’s domains. A domain administrator sees only the domains they hold.
POST/tenants/{tenant}/domains201Add a domain. Body: name, in any spelling; stored in ASCII form. It arrives unverified, with its token.
GET/tenants/{tenant}/domains/{name}200One domain, with its token and ownership.
POST/tenants/{tenant}/domains/{name}/verify200Mark the domain verified, and check its ownership again at once.
GET/tenants/{tenant}/domains/{name}/dkim200The domain’s DKIM keys: selector, algorithm, record to publish, when made, whether retired. Never the private half.
POST/tenants/{tenant}/domains/{name}/dkim201Make a new RSA and Ed25519 pair. Body: optional selector. Answers with items and the records to publish.
DELETE/tenants/{tenant}/domains/{name}/dkim/{selector}200Retire one key. It signs nothing more.

Accounts

MethodPathSuccessPurpose
GET/tenants/{tenant}/accounts200Every principal: users, groups, aliases and resources. A domain administrator sees those in their domains.
POST/tenants/{tenant}/accounts201Create a principal. Body: address (required), displayName, kind (user, group, alias or resource), and for a group members and restriction (anyone, tenant or members), for an alias target.
GET/tenants/{tenant}/accounts/{id}200One principal.
PATCH/tenants/{tenant}/accounts/{id}200Set a principal’s status. Body: status (active, disabled or deprovisioned).
DELETE/tenants/{tenant}/accounts/{id}204Deprovision a principal. Its mail is kept.
PUT/tenants/{tenant}/accounts/{id}/password204Give the account a new password. Body: password. The audit log records the change, never the password.

Domains and accounts shows each of these with examples and answers.

Settings

MethodPathSuccessPurpose
GET/tenants/{tenant}/settings200The settings document: version, updated_at, by, settings. Tenant 0 is the whole server’s, which only the operator reaches.
PUT/tenants/{tenant}/settings200Replace the document. Body: settings (object, required) and version (optional; refused with 409 if not current).
PATCH/tenants/{tenant}/settings200Change some settings. Body: the settings by key, null to remove one, and optional version.
GET/tenants/{tenant}/settings/history200Every version, newest first: version, updated_at, by. Query: limit (20 unless given, at most 200).
GET/tenants/{tenant}/settings/history/{version}200One earlier version, whole.

Runtime settings lists every key.

Queue and trace

MethodPathSuccessPurpose
GET/queue200Every message still in flight, soonest due first, at most 200. Query: tenant.
GET/queue/{id}200One message in flight, with each recipient’s state and last reply. Query: tenant.
GET/trace200The messages an address sent or received on a day. Query: address and day (both required), tenant.
GET/trace/{id}200One message’s whole story. Query: tenant.

tenant is the caller’s own tenant unless an operator names one. For /queue, an operator who names none sees every tenant’s messages; for /trace, an operator must name one. See Message trace for the fields.

Reports

MethodPathSuccessPurpose
GET/reports200DMARC aggregate and TLS reports other servers sent about a domain, for a day. Query: domain and day (both required), kind (dmarc or tls).

See Email authentication for the fields.

Audit log

MethodPathSuccessPurpose
GET/audit200The audit log, oldest first, with written, the count of every line ever written. Query: tenant, after (carry on after this id), limit (at most 200).

See Roles and the audit log.

Documentation

MethodPathSuccessPurpose
GET/openapi.json200The OpenAPI document. No credentials needed.

Errors

Every error is an RFC 9457 problem document, with the content type application/problem+json:

{
  "detail": "example.org is already taken",
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

detail says what went wrong in words you can act on. It never says more about the server than the caller is entitled to know: a queue id or trace id that belongs to someone else gets the same 404 as one that does not exist.

When the problem is at one place in what you sent, errors says where, as RFC 9457 §3 writes it: each entry repeats the detail and adds a pointer into the body, such as #/until, or #/delegates/1 for the second delegate in a list. A form can show the words beside the field they are about. A problem about the request as a whole has no errors.

{
  "detail": "`until` is later than now: a hold that has already ended holds nothing",
  "errors": [
    {
      "detail": "`until` is later than now: a hold that has already ended holds nothing",
      "pointer": "#/until"
    }
  ],
  "status": 400,
  "title": "Bad request",
  "type": "about:blank"
}
StatustitleTypical detailWhat to do
400Bad request`name` must be text, `day` is YYYY-MM-DD, the body is not JSON: <reason>, that is larger than an administrative request may beFix the request.
401Unauthorizedno credentials were offered, that token has expired, that token is not for the administrative API, that token is for another tenant, that token was refused, an operator token cannot belong to a tenantSend a valid token, or use the local socket.
403Forbiddenthat belongs to another tenant, that domain is not one of yours, that is somebody else's, a <role> may not do thatThe caller’s role does not reach this.
404Not foundno such collection, no such tenant, account <id> does not exist, this server answers under /api/v1Check the path and ids.
405Method not allowedthat is not something you can do to <thing>Use a method the path takes.
409Conflict<name> is already taken, <address>: that domain is not this tenant's, version <n> is no longer current; it is <m> now. Read it again and retry.Change what you asked for, or read the current state and retry.
500Server errorThe store’s own messageLook at the server’s log.

Requests that change something and are refused or fail are recorded in the audit log with the reason.

Something unclear or out of date on this page? Tell us.