API

Every operation exists twice, with the same permission checks: in REST (described by OpenAPI) and in GraphQL. Both are served by every kernoid process with the api role, for the instance the request’s host belongs to.

  Where Description
REST /v1/… OpenAPI 3.1 at /openapi.json on every instance – also here
GraphQL POST /graphql schema: schema.graphql

Both files are generated from the code (kernoid openapi, kernoid graphql-schema); CI checks that this site has the current ones.

Requests

  • The instance is chosen by the host: send requests to a domain of the instance (behind a reverse proxy, the forwarded host counts only from trusted proxies).
  • Authentication: Authorization: Bearer <session token> – the token from POST /v1/sessions. The session’s account acts; what it may do follows from its roles.
  • In a browser, the session can live in an HttpOnly cookie instead ("cookie": true at login, see Login); a change made with the cookie needs the header kernoid-csrf and an Origin of the instance, otherwise 403 with urn:kernoid:problem:csrf.
  • The tenant administered is the account’s own; instance administrators name another one with the header kernoid-tenant: <tenant id>.
  • Request bodies are JSON, at most 2 MiB; a request may take 30 seconds.
  • Every response carries an x-request-id, which the server’s log carries as well.

Errors

REST errors are problem details (RFC 9457, application/problem+json): type, title, status, detail. Internal details never appear in a response – they go to the log, findable by the request id.

Status Means
400 the request is not acceptable: a malformed value, or a rule refuses it (e.g. revoking the last tenant_admin)
401 no valid session, or wrong credentials; with type urn:kernoid:problem:login-required a fresh login is needed, with urn:kernoid:problem:mfa-required a second factor
403 the session is valid, but the permission named in detail is missing; or the account is deactivated
404 it does not exist – or it belongs to a tenant the caller cannot see
409 it exists already, or a domain or address is in use
429 too many failed attempts; see Retry-After
503 PostgreSQL or object storage is not reachable

GraphQL errors carry the same as a code in extensions.code: UNAUTHENTICATED, LOGIN_REQUIRED, MFA_REQUIRED, FORBIDDEN, NOT_FOUND, CONFLICT, BAD_REQUEST, TOO_MANY_ATTEMPTS (with retryAfter), UNAVAILABLE. Queries are limited in depth (16) and complexity (1000).

GraphQL

Queries read, mutations change; names follow the REST operations in camelCase – GET /v1/groups is groups, POST /v1/groups is addGroup, PATCH /v1/accounts/{id} is updateAccount, PUT /v1/roles/{role}/{kind}/{id} is grantRole. Several mutations in one request run one after the other:

mutation {
  addGroupMember(group: "…", account: "…")
  grantRole(role: "user_manager", principalKind: "group", principalId: "…")
}

REST operations

Sessions

Method Path What it does
GET /v1/sessions The valid sessions of the bearer token’s account, newest first.
POST /v1/sessions Logs in: checks address and password at the instance of this host and creates a session.
GET /v1/sessions/current The session of the bearer token.
DELETE /v1/sessions/current Logs out: ends the session of the bearer token.
POST /v1/sessions/current/logout Logs out: ends the session of the bearer token. If it came from a SAML provider with single logout, the answer says where to send the browser next.
POST /v1/sessions/current/passkey/finish Finishes a passkey check of the current session.
POST /v1/sessions/current/passkey/start Starts a passkey check of the current session.
POST /v1/sessions/current/recovery Uses a recovery code as a further check of the current session.
POST /v1/sessions/current/totp Checks a TOTP code as a further check of the current session.
POST /v1/sessions/passkey/finish Finishes a passkey login and creates a session.
POST /v1/sessions/passkey/start Starts a login with a passkey – no address, no password.
DELETE /v1/sessions/{id} Ends a session of the bearer token’s own account – e.g. one left open on another device.

One’s own account

Method Path What it does
PATCH /v1/account Changes the display name of one’s own account.
GET /v1/account/app-passwords The app passwords of one’s own account (never the passwords themselves).
POST /v1/account/app-passwords Makes an app password for one’s own account (needs a recent login); the answer holds it, this once.
DELETE /v1/account/app-passwords/{id} Removes an app password of one’s own account: the program using it can no longer log in.
GET /v1/account/collections One’s own calendars and address books, and those shared with one, with one’s level on each.
GET /v1/account/collections/{owner}/{collection}/access Who may use a calendar or address book one owns or administers (admin).
PUT /v1/account/collections/{owner}/{collection}/access Shares a calendar or address book one owns or administers with an account or a group of one’s tenant.
DELETE /v1/account/collections/{owner}/{collection}/access Stops sharing a calendar or address book with someone.
GET /v1/account/consents The applications one’s own account agreed to, with the scopes.
DELETE /v1/account/consents/{application} Withdraws one’s own consent to an application, and the application’s tokens – offline access included.
GET /v1/account/factors The second factors of one’s own account – for the login UI to ask for one, or to have one set up.
POST /v1/account/idp-links Starts linking another provider to one’s own account (RFC 0003, 3); answers with where to send the browser.
GET /v1/account/mail/access Who may use one’s own mail.
PUT /v1/account/mail/access Shares one’s own mail – the whole store or one mailbox – with an account or a group of one’s tenant.
DELETE /v1/account/mail/access Stops sharing one’s own mail with someone.
GET /v1/account/mail/shared The mail shared with one’s own account: other accounts’ stores one may use.
GET /v1/account/passkeys The passkeys of one’s own account.
POST /v1/account/passkeys/finish Finishes registering a passkey.
POST /v1/account/passkeys/start Starts registering a passkey for one’s own account (needs a recent session).
DELETE /v1/account/passkeys/{id} Removes a passkey of one’s own account.
PUT /v1/account/password Changes the password of one’s own account: needs a recent session and the current password. Every other session of the account ends.
POST /v1/account/recovery-codes Generates new recovery codes for one’s own account (needs a recent session); earlier ones stop working.
POST /v1/account/totp Proposes a TOTP secret for one’s own account (needs a recent session).
DELETE /v1/account/totp Removes TOTP from one’s own account.
POST /v1/account/totp/confirm Confirms the proposed TOTP secret with a first code.

Login UI

Method Path What it does
GET /v1/auth-requests/{id} A pending auth request – for the login UI to show who is asking.
POST /v1/auth-requests/{id} Finishes an auth request with the session of the bearer token; answers with the URL to send the browser to. If the application needs consent, the answer is 403 with type urn:kernoid:problem:consent-required: show what it asks for, and finish again with {"consent": true}.
POST /v1/auth-requests/{id}/decline Ends an auth request without a login – no session for prompt=none, or the person declined; answers with the URL that tells the application.
POST /v1/idp-logins Starts a login at an identity provider; answers with where to send the browser.
POST /v1/idp-logins/link Links the provider login of a proposal to the account of the session – the person logged in to it to prove it is theirs; from then on the provider logs in as that account.
POST /v1/idp-logins/proposal What a provider login that found an account with its verified address proposes (RFC 0003, 3): log in to that account to link it – or, where registration is open, create a separate one. Does not use the code up.
POST /v1/idp-logins/register Creates a separate account for the provider login of a proposal instead of linking it, where registration is open, and logs it in.
POST /v1/idp-logins/session Exchanges the result of a login at an identity provider for a session – once.
GET /v1/login-options Where an address logs in: at an identity provider its domain is routed to, or with kernoid’s own login. Says nothing about whether the account exists.
GET /v1/login-providers The providers offered to everyone on the login page (“Sign in with …”).
GET /v1/registration What registering offers at this instance; the providers to register with are at /v1/login-providers.
POST /v1/registrations Starts a registration: checks it, refuses what is taken (409: log in instead), and sends a code to the email address. Nothing is stored as an account before the code is confirmed.
POST /v1/registrations/{id}/confirm Finishes a registration with the code from the message: the account is created.

The tenant

Method Path What it does
GET /v1/tenant The tenant administered (needs directory.read).
PATCH /v1/tenant Renames the tenant (needs tenant.manage).
POST /v1/tenant/domains Claims a mail domain (needs domains.manage); answers with the TXT record that proves it.
DELETE /v1/tenant/domains/{domain} Removes a mail domain – unless addresses use it (needs domains.manage).
POST /v1/tenant/domains/{domain}/verify Verifies a claimed domain by its TXT record (needs domains.manage), or marks it verified without DNS (trust, needs tenants.manage).
GET /v1/tenant/login-policy How the tenant’s accounts log in (needs directory.read).
PUT /v1/tenant/login-policy Sets how the tenant’s accounts log in (needs policies.manage).
GET /v1/tenant/mail-dns The tenant’s verified domains as mail domains of kernoid: the DNS records to publish (MX, SPF, DKIM, DMARC) and whether they are published (needs directory.read; reads DNS, changes nothing).
PUT /v1/tenant/modules/{module} Switches a module on or off for the tenant (needs tenant.manage). The module must be compiled in; whether it runs is decided by the installation’s configuration.
GET /v1/tenant/permissions The permissions of the caller for the tenant administered – for a UI to show what it may do.
PUT /v1/tenant/policy Sets whether every login to an application of the tenant needs a second factor, and how long accounts without one may still log in (needs policies.manage).
GET /v1/tenant/registration Whether people may register with the tenant, and on which domains (needs directory.read).
PUT /v1/tenant/registration Opens or closes registration, and names the domains new accounts may take an address on (needs policies.manage).
GET /v1/tenant/service-policies The tenant’s rules for each service (needs directory.read).
PUT /v1/tenant/service-policies/{service} Sets who gets a service when an account is created, and whether it needs approval (needs policies.manage).

Accounts

Method Path What it does
GET /v1/accounts The tenant’s accounts (needs directory.read).
POST /v1/accounts Creates an account (needs accounts.manage).
GET /v1/accounts/{id} One account (needs directory.read).
PATCH /v1/accounts/{id} Changes an account’s display name, login name or active state (needs accounts.manage).
PUT /v1/accounts/{id}/aliases/{address} Adds an alias on a verified domain of the tenant (needs accounts.manage).
DELETE /v1/accounts/{id}/aliases/{address} Removes an alias; the login name cannot be removed (needs accounts.manage).
GET /v1/accounts/{id}/consents The applications an account agreed to (needs accounts.manage).
DELETE /v1/accounts/{id}/consents/{application} Withdraws an account’s consent to an application and the application’s tokens for it (needs accounts.manage and every permission the account holds).
PUT /v1/accounts/{id}/contact Sets an account’s contact address (needs accounts.manage).
GET /v1/accounts/{id}/links The external identities linked to an account (needs directory.read).
DELETE /v1/accounts/{id}/links/{idp}/{subject} Unlinks an external identity from an account (needs accounts.manage).
PUT /v1/accounts/{id}/password Sets an account’s password and ends its sessions (needs accounts.manage and every permission the account holds).
DELETE /v1/accounts/{id}/second-factors Removes all TOTP, passkeys and recovery codes of an account (needs accounts.manage and every permission the account holds).
GET /v1/accounts/{id}/services The services of an account (needs directory.read).
PUT /v1/accounts/{id}/services/{service} Grants a service – also one an account requested (needs accounts.manage); mail only to an account with an address of the tenant.
DELETE /v1/accounts/{id}/services/{service} Takes a service away (needs accounts.manage); a mail store stays until the account is removed.
GET /v1/accounts/{id}/sessions The account’s valid sessions (needs accounts.manage).
DELETE /v1/accounts/{id}/sessions/{session} Ends one session of the account (needs accounts.manage and every permission the account holds).
PUT /v1/accounts/{id}/username Gives an account a username, or takes it away (needs accounts.manage; the tenant must use usernames).
POST /v1/external-accounts Creates an account without an address of the tenant: for logins only – and files, if granted (needs accounts.manage).

Mail

Method Path What it does
GET /v1/accounts/{id}/mail An account’s mail: usage, quota and mailboxes (needs accounts.manage).
GET /v1/accounts/{id}/mail/access Who may use an account’s mail (needs accounts.manage).
PUT /v1/accounts/{id}/mail/access Grants an account or a group of the tenant access to an account’s mail, replacing an earlier grant there (needs accounts.manage).
DELETE /v1/accounts/{id}/mail/access Removes a grant on an account’s mail (needs accounts.manage).
POST /v1/accounts/{id}/mail/import Stores a message (RFC 5322, the request body) in a mailbox of an account (needs accounts.manage).
GET /v1/accounts/{id}/mail/mailboxes/{mailbox}/mbox A mailbox of an account (by id or role) as mbox, with its keywords (needs accounts.manage).
POST /v1/accounts/{id}/mail/mailboxes/{mailbox}/mbox Stores the messages of an mbox (the request body, up to 2 MiB; larger ones with the CLI) in a mailbox of an account (by id or role), with their keywords (needs accounts.manage).
PUT /v1/accounts/{id}/mail/quota Sets an account’s mail quota; null for the tenant’s default (needs accounts.manage).
GET /v1/accounts/{id}/mail/sieve An account’s Sieve scripts (needs accounts.manage).
PUT /v1/accounts/{id}/mail/sieve-active Chooses the Sieve script that runs; null for none (needs accounts.manage).
PUT /v1/accounts/{id}/mail/sieve/{name} Saves a Sieve script, checked first (needs accounts.manage).
DELETE /v1/accounts/{id}/mail/sieve/{name} Removes a Sieve script (needs accounts.manage).
GET /v1/instance/mail-queue The queue of outgoing mail of the instance’s tenants, newest first (needs instance.read).
POST /v1/shared-mailboxes Creates a shared mailbox: an account nobody logs in as, used through granted access (needs accounts.manage).
GET /v1/tenant/mail-quota The tenant’s default mail quota (needs directory.read).
PUT /v1/tenant/mail-quota Sets the tenant’s default mail quota, for every account without its own; null for no limit (needs tenant.manage).
GET /v1/tenant/mail-retention The tenant’s mail retention (needs directory.read).
PUT /v1/tenant/mail-retention Sets how many days emails stay in the accounts’ Trash and Junk; null keeps them (needs tenant.manage).

Groups

Method Path What it does
GET /v1/groups The tenant’s groups with their members (needs directory.read).
POST /v1/groups Creates a group (needs groups.manage).
DELETE /v1/groups/{id} Removes a group; refused while it holds roles (needs groups.manage).
PATCH /v1/groups/{id} Renames a group (needs groups.manage).
PUT /v1/groups/{id}/addresses/{address} Gives a group an address on a verified domain of the tenant; mail to it goes to every member (needs groups.manage).
DELETE /v1/groups/{id}/addresses/{address} Removes an address of a group (needs groups.manage).
PUT /v1/groups/{id}/members/{account} Adds an account to a group (needs groups.manage, and for a group holding roles the permission to assign them).
DELETE /v1/groups/{id}/members/{account} Removes an account from a group (needs the same as adding one).

Roles

Method Path What it does
GET /v1/roles The roles there are, and who holds which in the tenant (needs directory.read).
PUT /v1/roles/{role} Defines a role of the tenant’s own, or changes its permissions (needs roles.manage).
DELETE /v1/roles/{role} Deletes a role of the tenant’s own, once nobody holds it (needs roles.manage).
PUT /v1/roles/{role}/{kind}/{id} Assigns a role to an account or a group of the tenant (needs roles.manage).
DELETE /v1/roles/{role}/{kind}/{id} Takes a role back; the tenant’s last tenant_admin assignment cannot be revoked (needs roles.manage).

Projects

Method Path What it does
GET /v1/granted-projects The projects other tenants have granted to this one, with the roles it may give (needs directory.read).
GET /v1/projects The tenant’s projects with their roles (needs directory.read).
POST /v1/projects Creates a project (needs projects.manage).
GET /v1/projects/{id} One project (needs directory.read).
DELETE /v1/projects/{id} Removes a project – once it has no applications and nobody holds its roles (needs projects.manage).
PATCH /v1/projects/{id} Renames a project or changes its settings (needs projects.manage).
GET /v1/projects/{id}/authorizations Who holds which role of the project – for a granted project only the tenant’s own (needs directory.read).
PUT /v1/projects/{id}/authorizations/{role}/{kind}/{principal} Gives an account or group of the tenant a role of the project – of its own, or one granted to it (needs authorizations.manage).
DELETE /v1/projects/{id}/authorizations/{role}/{kind}/{principal} Takes a project role back (needs authorizations.manage).
GET /v1/projects/{id}/grants The tenants the project is granted to (needs directory.read).
PUT /v1/projects/{id}/grants/{tenant} Grants the project to another tenant of the instance – named by its id or a verified mail domain – with some of its roles, or changes them (needs projects.manage).
DELETE /v1/projects/{id}/grants/{tenant} Takes a grant back, once that tenant holds none of the project’s roles (needs projects.manage).
PUT /v1/projects/{id}/roles/{key} Adds a role to the project, or changes its display name and group (needs projects.manage).
DELETE /v1/projects/{id}/roles/{key} Removes a role from the project, once nobody holds it (needs projects.manage).

Applications

Method Path What it does
GET /v1/applications The tenant’s applications (needs directory.read).
POST /v1/applications Registers an application in a project of the tenant (needs applications.manage).
GET /v1/applications/{id} One application (needs directory.read).
PATCH /v1/applications/{id} Changes an application (needs applications.manage).
PUT /v1/applications/{id}/options Sets the application’s options: token format, offline access, consent, logout notifications (needs applications.manage).
PUT /v1/applications/{id}/policy Sets whether logins to the application need a second factor, on top of the tenant’s policy (needs applications.manage).
POST /v1/applications/{id}/secret Replaces the secret of a confidential application (needs applications.manage).

Identity providers

Method Path What it does
GET /v1/identity-providers The tenant’s identity providers, and those of the instance that serve every tenant (needs directory.read).
POST /v1/identity-providers Connects an identity provider (needs identity_providers.manage; for the whole instance, instance.manage). Register the redirect_uri from the answer of GET /v1/identity-providers/{id} at the provider.
GET /v1/identity-providers/{id} One identity provider (needs directory.read).
DELETE /v1/identity-providers/{id} Removes an identity provider, once no account is linked to it (needs identity_providers.manage).
PATCH /v1/identity-providers/{id} Changes an identity provider’s settings, or switches it off and on (needs identity_providers.manage).
POST /v1/identity-providers/{id}/check Fetches the provider’s discovery document and keys: whether kernoid reaches it as configured (needs identity_providers.manage).
PUT /v1/identity-providers/{id}/secret Sets the client secret (needs identity_providers.manage).
DELETE /v1/identity-providers/{id}/secret Removes the client secret – for providers that take PKCE alone (needs identity_providers.manage).

Provisioning (SCIM tokens)

Method Path What it does
GET /v1/scim-tokens The tenant’s SCIM tokens (needs directory.read).
POST /v1/scim-tokens A new SCIM token for the tenant’s directory (needs provisioning.manage).
DELETE /v1/scim-tokens/{id} Revokes a SCIM token (needs provisioning.manage).

The instance

Method Path What it does
GET /v1/instance The instance reached under the host of this request.
PATCH /v1/instance Renames the instance (needs instance.manage).
GET /v1/instance/details The instance with all its domains (needs instance.read).
PUT /v1/instance/domains/{domain} Reaches the instance under another domain as well (needs instance.manage).
DELETE /v1/instance/domains/{domain} Removes a domain; not the primary one (needs instance.manage).
PUT /v1/instance/login-ui Sends logins to a login UI of the instance’s own, which may then call the API from its origin (needs instance.manage).
PUT /v1/instance/passkey-rp-id Binds passkeys to another RP ID – one of the instance’s domains or a parent domain of one (needs instance.manage). Passkeys registered under the former RP ID stop working.
PUT /v1/instance/primary-domain Sets the domain the issuer and links use (needs instance.manage). Passkeys are bound to it.
PUT /v1/instance/registration-tenant Names the tenant where people without an address of a tenant register (needs instance.manage).
GET /v1/instance/roles The instance roles and who holds them (needs instance.read).
PUT /v1/instance/roles/{role} Defines an instance role of the instance’s own, or changes its permissions (needs instance_roles.manage).
DELETE /v1/instance/roles/{role} Deletes an instance role of the instance’s own, once nobody holds it (needs instance_roles.manage).
PUT /v1/instance/roles/{role}/{kind}/{id} Assigns an instance role to an account or group of any tenant of the instance (needs instance_roles.manage).
DELETE /v1/instance/roles/{role}/{kind}/{id} Takes an instance role back; the last instance_admin stays (needs instance_roles.manage).
GET /v1/instance/signing-keys The instance’s signing keys (needs instance.read).
POST /v1/instance/signing-keys Creates a signing key: published at once, signing once activated (needs instance.manage).
DELETE /v1/instance/signing-keys/{kid} Removes an inactive key whose tokens have expired (needs instance.manage).
POST /v1/instance/signing-keys/{kid}/activate Signs with this key from now on (needs instance.manage).
GET /v1/instance/tenants The instance’s tenants (needs instance.read).
POST /v1/instance/tenants Creates a tenant (needs tenants.manage).
PATCH /v1/instance/tenants/{id} Renames, deactivates or reactivates a tenant (needs tenants.manage); not one’s own by deactivation.

Health

Method Path What it does
GET /healthz Liveness: the process runs. Says nothing about its dependencies, so an orchestrator does not restart every process when the database is away.
GET /readyz Readiness: PostgreSQL and object storage answer. Details of failures go to the log, not to the caller.

Meta

Method Path What it does
GET /version The name and version of this kernoid.

OpenID Connect

The protocol endpoints (/.well-known/openid-configuration, /oauth/v2/…, /oidc/v1/…) are standard OpenID Connect and OAuth 2.0 and not part of the OpenAPI description – see Login and OpenID Connect. The same holds for SCIM 2.0 at /scim/v2 and the identity provider endpoints /idp/callback, /idp/saml/acs and /idp/saml/metadata – see Identity providers and SCIM.