/admin/v1 is the operational surface: the endpoints scripts and monitoring call — deploy verification, bans, the queue, config overrides. Administrative data objects (organizations, audit log, journaling rules, admin principal management) live on admin-over-JMAP instead (urn:oximail:params:jmap:v2:admin, ADR-075) — new admin objects are added there, not here.
Authentication
Every endpoint requires the admin bearer token from[admin] token:
[server] admin_bind if you prefer it off the public listener.
GET /health is the one route below that is not under /admin/v1: it takes an ordinary session bearer token, not the admin token.
Endpoint map
Deploy & health
Accounts and organizations
Security
Mail operations
Configuration
What lives on admin-over-JMAP
The data objects an admin console binds to are JMAP methods underurn:oximail:params:jmap:v2:admin (ADR-075), gated on an admin session: Tenant/get|set, Principal/getAdmin|setAdmin|queryAdmin|changesAdmin, Principal/setPasswordAdmin, Principal/enableMail (sharing), AccountStorage/get, DeviceSession/queryAdmin|getAdmin|setAdmin, AppPassword/queryAdmin|getAdmin|setAdmin, ErasureJob/queryAdmin|getAdmin (compliance), HealthReport/query|get; the administration log, AuditEvent/*, has its own capability (compliance). The facts worth knowing before binding a form:
Tenant/setwritesplan,offlinePersistenceAllowed,customEmojiEnabled,maxFileVersionsPerNode(UnsignedInt | null, refused by name above the instance ceiling of 100; files),defaultLanguage(en,fr,de,it) andlegalIdentity, the controller the organization declares, written as a whole (nullclears it; a hostile value is refused naming its field,legalIdentity/legalName; the instance’s processing countries are not the organization’s to write);Tenant/getprojects them withmaxPrincipals,resourceLimitsOverride,defaultTimezoneandeffectiveLegalIdentity, read-only, each value with its source (tenant,instance,derived), see the legal identity. The tenant carries no storage quota since ADR-151: a quota is set per account. A create or a destroy sent toTenant/set(or toPreferences/set) is refused by name instead of being ignored. Every mutation is journaled, and a change movessessionState, so an open client re-reads the session.- A principal id and its account id are different UUIDs.
Principal/getAdminserves derived properties:accountId, the join key towardAccountStorage, and the account’s two quotas,maxStorageBytes(mail) andmaxDriveBytes(Drive;null= the deployment default,0= no limit), so a value written byPrincipal/setAdmin {maxStorageBytes}or{maxDriveBytes}reads back on the same object,nullincluded. Role and status patches on a principal are two-phase (reads and account-plane writes first, then the row writes under one transaction), after a deadlock that had every such patch dying insqlite busyon every box while the test suite stayed green on a mocked plane.Principal/getAdminalso servesisEntity(entities). - The
Principal/setAdminpatch is a closed vocabulary. A key outside it is refusedinvalidProperties, every offending key named with the list of what is settable, instead of being dropped while the answer saidupdated; apasswordkey is pointed toPrincipal/setPasswordAdmin. A non-emptypermissionslist is refused on update as on create (no authorization code reads it); the empty list older clients echo stays accepted. - A departure is a patch on a suspended principal. In the same patch as
status: "suspended", or on a principal already suspended:forwardTo(the Principal id of the colleague the leaver’s address now reaches, as an exact alias of that address) with an optionalforwardUntil, andmailboxGrantTo(the colleague who reads the leaver’s mailbox, a read-only grant the organization wrote) with agrantUntilthat is required and at most one year out.nullwithdraws either, at any time and in any state. Every refusal (a colleague who is not a Principal of the organization, a date without its target, a target already disabled) is decided before the freeze, so a refused successor never leaves a suspended account without its redirect.Principal/getAdminserves what stands asoffboarding({forwardTo, forwardUntil, mailboxGrantTo, grantUntil}, ornull). It is the same engine asoximail account offboard, and it takes the same second-factor check as a role change. Principal/setPasswordAdmin {id, password}sets someone else’s password without the old one. It is a dedicated method, never a patch key, and it is behind the strict second-factor gate: an administrator with no passkey enrolled is refused, not warned, and the session must have been opened with that passkey. The hash, the closing of every open access of the account and the audit line in the subject’s chain commit together or not at all. Closing every access means more than the tokens: every app password and every passkey of the account is deleted, and every active rule that forwards mail is disabled with the forwarded copies still queued marked failed, since a forward outlives a stolen password (forwards). The answer carriestokensRevoked,appPasswordsRevoked,passkeysRevoked,forwardsSuspendedandforwardCopiesCancelled, never the password;oximail account set-passwordandPUT /admin/v1/accounts/{id}do the same. A person changing their own password closes none of that. A shared account never carries a password (forbidden). There is no forced change at first login: the same hash serves IMAP, SMTP, CalDAV and SMB, which cannot show a screen.DeviceSession/*AdminandAppPassword/*Adminsee and cut the device sessions and app passwords of another account:queryAdmin {filter: {principalId}}(the one filter, required) lists the ids,getAdmin {ids}serves the metadata,setAdmin {destroy}cuts;createandupdatedo not exist, since a session is opened by authentication and an app password is minted by its holder, who alone sees the secret. A session carriesdeviceLabel,userAgent,authMethod,createdAt,lastSeenAt,expiresAt, and what support needs to answer “my phone gets no notifications”:hasPushSubscription(a verified, unexpired Web Push subscription for that device),pushLastDeliveredAt(when the push service last accepted a notification for it, recorded at most once every ten minutes) andpushEndpointOrigin(the scheme and host of the push service, Apple, Google or Mozilla, never the rest of the URL, which is a delivery secret; a subscription the service dropped is no longer live but keeps its origin); an app passwordname,scope(a custom list is served asscope: "custom"with the URNs incapabilities),readOnly,createdAt,lastUsedAt, never the secret nor its hash. Cutting an app password also closes every bearer token it opened. No second-factor gate: these methods only ever remove access. Each cut is an audit line in the chain of the person it happened to, with the administrator as actor.- An instance operator is changed at the console only.
Principal/setPasswordAdmin, aPrincipal/setAdminupdate and adestroyinDeviceSession/setAdminorAppPassword/setAdminthat target the account of an instance operator are refusedforbidden(the other ids of a batch go through): resetting that password would be reading the whole box. Revoke the privilege at the console first. HealthReport/query|getserve thedoctorround of the box. A timer (oximail-health.timer, every 15 minutes, run as the service user;oximail setup health-timer --applylays it on a box installed before it existed) recordsoximail doctor --json --record, and reports older than 30 days are purged. A report carriesid,takenAt,durationMs,exitCode,status(ok,degraded,failed),ageSeconds,stale(older than twice the cadence: a dead timer reads “last checked three days ago”, never “all is well”) andreport, the exact--jsonarray:check,status,detail, plusfamily(mail,backup,storage,security,accounts,system,extension),factswith the dated facts as UTCDates (observedAtof the last backup or recovery drill,expiresAtof the certificate) and, on awarnor afail,remedy, the verb to type.queryfilters onafterandbefore(UTCDates) and sorts ontakenAt;get {ids: null}answersrequestTooLargebeyondmaxObjectsInGet, so the latest report is aquery(takenAtdescending,limit: 1) followed by aget. A tenant admin reads the reports taken while its organization was alone on the box, which is every report of a single-organization box; a report taken while the box carried two organizations stays out of its reach for good, and only an instance operator reads it. A member is refusedaccountNotSupportedByMethodat the dispatch gate; an account that carries the admin capability by an override without being an administrator getsforbidden.detailquotes values an organization writes (addresses, aliases): render it as plain text only.
Conventions
- Responses are JSON; errors carry a machine-readable reason and an HTTP status that means what it says.
- Everything here is also reachable from the CLI — both are thin adapters over the same orchestration, so behaviour is identical.
- The API is per organization where data is organization-scoped; there is deliberately no cross-tenant data view.