Skip to content

Authentication ​

The web interface authenticates with a session cookie. A program authenticates with an API key: a secret created by a member, sent on every request, which acts on behalf of that member within the scopes it was given. This page covers the key; the session cookie is described at the end, for completeness.

Creating a key ​

Each member creates their own keys in Settings › API keys. A key has:

  • a name, to recognise it later (1 to 120 characters);
  • the scopes it may use, chosen domain by domain: nothing, read only, or read and write. See Scopes. A key carries at most 20 scopes, and at least one;
  • an optional expiry: 30, 90 or 365 days in the interface (the API accepts 1 to 730 days through expiresInDays). Without expiry, the key lives until it is revoked.

The secret is shown once, when the key is created. Mankomail stores only a hash of it: nobody, not even an administrator, can read it again. Copy it before closing the dialog; if it is lost, revoke the key and create another. The secret starts with mk_; the list of keys shows only its first eight characters as a landmark.

Keys can also be created over the API, by a session only: POST /api/v1/api-keys with name, scopes and optionally expiresInDays. A key cannot create keys (see What refuses a key).

Administrators see every key of the instance, with the member each one belongs to, under Administration › API keys, and can revoke any of them. Creating and revoking a key are recorded in the audit log (api_key.created, api_key.revoked).

Sending the key ​

Send the secret in the Authorization header, as a bearer token, on every request:

http
GET /api/v1/workflows HTTP/1.1
Host: <PUBLIC_BASE_URL>
Authorization: Bearer mk_Qm9uam91cl9jZXN0X3VuX2V4ZW1wbGVfZGVfY2xl
Accept: application/json

A request that carries an API key needs no cookie and no CSRF token. The session cookie protects itself with SameSite=Lax; a key is never sent by a browser on its own, so there is nothing to protect against.

When a request carries an Authorization: Bearer header, the key decides: an invalid key is refused even if a valid session cookie travels in the same request. An explicit header is an explicit intention; falling back silently to a cookie would hide mistakes.

The key acts as its member ​

For the route, a request authenticated by a key is a request of the member who created it. The key sees the member's workflows, mailboxes, tables and executions, and nothing else: a workflow of another member is a 404, as it would be for the member in the interface. Administration routes (/api/v1/admin/…) require the member to be an administrator: an admin:* scope on the key of an ordinary member grants nothing, and the interface does not let an ordinary member tick it.

Scopes therefore never add rights; they only remove some. A key limited to executions:read of an administrator cannot invite a member, even though its owner could.

Actions performed with a key are attributed to its member in the audit log. The key's lastUsedAt is updated as it is used, with a precision of one minute, so unused keys can be spotted and revoked.

Expiry and revocation ​

  • An expired key is refused like an unknown one. Its row stays in the list with its expiresAt, so you can see which integration needs a new key.
  • Revoking a key is immediate and final: the next request with it is refused. Revoking twice keeps the first date. The row is kept, for the audit log; it never disappears from the list.
  • A revoked or expired key cannot be reactivated. Create a new one and replace the secret in the calling system.

Rate limit ​

Each key may make 600 requests per sliding minute. The limit is per key, not per member: two integrations of the same member do not share a quota. Over the limit, the response is 429 with the code api_key.rate_limited, a Retry-After header in seconds and the same value in details.retryAfterSeconds. The limit is counted before the scope check: a script looping on a 403 costs as much as one looping on a 200.

Refusals ​

StatusCodeMeaning
401auth.unauthenticatedNo key, or a key that is unknown, malformed, expired or revoked. The four cases are deliberately indistinguishable.
403api_key.scope_missingThe key is valid but none of its scopes covers the route. details.required names the scope to add, for example "workflows:write". Create a key with that scope; the current one is not broken.
403api_key.session_requiredThe route is reserved to a member session.
403api_key.route_not_allowedThe route is not open to API keys at all.
403auth.forbiddenThe member behind the key lacks the role the route requires (an administration route called by an ordinary member's key).
429api_key.rate_limitedToo many requests in the last minute. Wait Retry-After seconds.

What refuses a key ​

A handful of routes make sense only for a person in a browser and refuse keys with 403 api_key.session_required:

  • managing your own keys: GET /api/v1/api-keys, POST /api/v1/api-keys, POST /api/v1/api-keys/{id}/revoke — a key that could create keys would be a key you can no longer revoke, because it would already have replaced itself;
  • POST /api/v1/auth/logout and POST /api/v1/me/password;
  • starting an OAuth connection in the browser, POST /api/v1/oauth/{provider}/start;
  • the WebSocket of the interface, /api/v1/ws.

Administrators can list and revoke keys over the API with an admin scope: GET /api/v1/admin/api-keys (admin:read) and POST /api/v1/admin/api-keys/{id}/revoke (admin:write).

In the reference, each route states its access rule: a scope, "member session only", "no authentication" (sign-in, health checks) or "authorised by the token in the URL" (approval links, webhook triggers).

A member signs in with POST /api/v1/auth/login, sending email and password. The response sets a cookie named session (HttpOnly, SameSite=Lax, Secure in production), which authenticates the following requests; GET /api/v1/auth/me returns the signed-in member and POST /api/v1/auth/logout ends the session. Sign-in attempts are rate-limited per IP address (429 auth.too_many_attempts with Retry-After), and a sign-in that does not arrive over HTTPS is refused in production (auth.https_required).

The cookie is meant for the interface. For anything automated, create a key.