Appearance
Authentication
Almost every endpoint requires a signed-in user. There is no anonymous read surface and no separate API-key system: a client authenticates as a person, with exactly the access that person has.
Two transports carry the same credential — a signed JWT. Which one you use depends on whether you are a browser.
Signing in
bash
curl -X POST https://api.kancil.nl/auth/login \
-H 'Content-Type: application/json' \
-d '{"username": "you", "password": "…"}'A successful response carries the public user record plus the token:
json
{
"username": "you",
"role": "viewer",
"access_token": "eyJhbGciOiJIUzI1NiIs…",
"token_type": "bearer"
}It also sets a kancil_session cookie: HttpOnly, SameSite=Lax, and Secure in any deployment worth the name. The token is valid for seven days.
Which one do I use?
In a browser, ignore access_token and let the cookie do the work — that is what the web app does, and an HttpOnly cookie cannot be read by page JavaScript, so an XSS bug cannot walk off with the session.
Everywhere else — scripts, native apps, integrations — store access_token and send it as a header. Cookies you cannot read are no use to you.
Sending the credential
Cookie, which any HTTP client with a cookie jar does for you:
bash
curl -c jar -X POST https://api.kancil.nl/auth/login -d '…'
curl -b jar https://api.kancil.nl/tasksOr the header:
bash
curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIs…' \
https://api.kancil.nl/tasksIf both are present the cookie wins — the header is only consulted when no session cookie is sent. Send neither and you get 401.
Multi-factor authentication
When the account has MFA enabled, POST /auth/login does not return a session. It returns an MFA challenge instead — as an mfa_token in the body, and as a short-lived kancil_mfa cookie for browsers. Send the code back to POST /auth/login/2fa along with that challenge to exchange it for a real session.
The challenge is valid for five minutes. It carries a distinct typ claim, so it cannot be replayed as a session token even though both are JWTs.
Roles
Two roles, in privilege order: viewer and admin. Endpoints tagged admin require the latter; everything else needs only a signed-in user. Self-registration, where enabled, always creates a viewer.
The role is not carried in the token — it is read from the database on every request, so a demotion takes effect immediately rather than at the user's next login.
Signing out
POST /auth/logout clears the cookie. A bearer token is not revocable: it stays valid until it expires, so treat a stored access_token with the same care as the password that produced it.