English
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/jsonA 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
| Status | Code | Meaning |
|---|---|---|
401 | auth.unauthenticated | No key, or a key that is unknown, malformed, expired or revoked. The four cases are deliberately indistinguishable. |
403 | api_key.scope_missing | The 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. |
403 | api_key.session_required | The route is reserved to a member session. |
403 | api_key.route_not_allowed | The route is not open to API keys at all. |
403 | auth.forbidden | The member behind the key lacks the role the route requires (an administration route called by an ordinary member's key). |
429 | api_key.rate_limited | Too 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/logoutandPOST /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).
The session cookie
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.