NoteBugsDocs

API reference

Authentication and session

The routes that answer without a credential: session, first access, sign in and out.

These four, plus /api/health, are the entire public list: they answer without a credential because they are the way to obtain one.

GET/api/auth/session
Role: none

Who this request is, which workspaces it reaches and whether the installation still awaits its first access.

Answers 200 always, even with no credential at all: “there is nobody” is an answer, not a failure. It is how the interface decides between drawing the board, the login screen or the first-access screen.

Request

curl -s http://localhost:3000/api/auth/session

Response200

{
  "user": {
    "id": "cmt94cjx5000vql01imegem2r",
    "name": "Ana",
    "email": "[email protected]",
    "role": "ADMIN",
    "avatarUrl": null,
    "jobTitle": null,
    "timezone": "",
    "notifyBackupReady": true,
    "hasApiToken": true,
    "apiTokenCreatedAt": "2026-08-25T20:37:29.998Z",
    "apiTokenLast4": "mGQ",
    "passwordPending": false,
    "lastLoginAt": "2026-08-27T19:50:42.947Z",
    "createdAt": "2026-08-25T20:29:57.593Z",
    "tenants": [
      {
        "id": "cmsp4djx60002p801o7ybpkv7",
        "name": "Pessoal",
        "color": "amber",
        "role": "ADMIN",
        "membership": null
      }
    ]
  },
  "setupRequired": false,
  "publicMode": false
}

Error responses

CodeWhen it happens
429Too many requests from this IP this minute. The response carries Retry-After with how many seconds to wait.

publicMode: true means the installation waives login: the identity is an anonymous visitor with role ADMIN and no record in the database.

POST/api/auth/setup
Role: none

Creates the FIRST account of the installation, which is born an admin.

The window in which this route accepts anything is exactly the interval between the installation coming up and the first account existing. After that, 409 forever, and there is no self-signup.

Request body

FieldTypeDescription
namerequiredstringThe name that shows up in the header and on comments.
emailrequiredstringThe account's identity: unique in the database, normalised to lowercase.
passwordrequiredstringFrom 8 to 200 characters. Leading and trailing spaces are part of it and are not trimmed.

Request

curl -s -X POST http://localhost:3000/api/auth/setup \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ana",
    "email": "[email protected]",
    "password": "uma senha longa"
  }'

Response201

{
  "user": {
    "id": "cmt94cjx5000vql01imegem2r",
    "name": "Ana",
    "email": "[email protected]",
    "role": "ADMIN",
    "tenants": []
  },
  "setupRequired": false,
  "publicMode": false
}

Error responses

CodeWhen it happens
409The installation already has an account. The first-access window closes on its own and never reopens.
422The body did not pass the schema. The details field carries zod's fieldErrors and formErrors, field by field.
429Too many requests from this IP this minute. The response carries Retry-After with how many seconds to wait.

The account created here becomes a member of every workspace that already exists, and the role does not come in the body: it is born ADMIN by definition.

POST/api/auth/login
Role: none

Trades e-mail and password for a session, delivered in an HttpOnly cookie.

The cookie is SameSite=Lax and gets Secure according to how the request arrived: on a desktop install at http://localhost it has to be accepted, and behind a TLS proxy it has to be Secure.

Request body

FieldTypeDescription
emailrequiredstringNormalised to lowercase before the query.
passwordrequiredstringNo length minimum on this route: any wrong credential gets the same answer.

Request

# -c stores the session cookie in the given file
curl -s -c cookies.txt -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]", "password": "uma senha longa" }'

Response200

{
  "user": {
    "id": "cmt94cjx5000vql01imegem2r",
    "name": "Ana",
    "email": "[email protected]",
    "role": "ADMIN",
    "tenants": []
  },
  "setupRequired": false,
  "publicMode": false
}

Error responses

CodeWhen it happens
401Invalid e-mail or password. The answer is the same sentence in all three failure cases, and it takes the same time.
422The body did not pass the schema. The details field carries zod's fieldErrors and formErrors, field by field.
429Too many sign-in attempts. The login limit is narrower (10 every 5 minutes) and the counter includes the target e-mail.

To consume the API from outside, prefer the API token: Authorization: Bearer. The cookie takes precedence when both arrive together.

POST/api/auth/logout
Role: none

Ends the session of this tab.

Idempotent and with no identity required: signing out while already out returns 200.

Request

curl -s -b cookies.txt -X POST http://localhost:3000/api/auth/logout

Response200

{ "ok": true }

Error responses

CodeWhen it happens
429Too many requests from this IP this minute. The response carries Retry-After with how many seconds to wait.