Authentication
Every endpoint under /api needs a credential, with these exceptions:
GET /api/setup/statusandPOST /api/setup, for the first-run setupPOST /api/auth/login
Pages, GET /img and GET /uploads/* are public. Without a valid credential, a protected endpoint answers 401 with { "message": "Unauthorized" }.
Credentials
| Kind | How you get it | Lifetime |
|---|---|---|
| Session | POST /api/auth/login, or the first-run setup |
30 days, extended each time it is used |
| Personal token | POST /api/token, from a session. See Users and tokens |
Until you delete it |
| OAuth token | An app such as an AI agent that you approved. See Connected apps | Access token 1 hour, refresh token 30 days |
Send the credential in the Authorization header:
curl http://localhost:3000/api/post \
-H "Authorization: Bearer njt:01a1aaf9-9531-75c3-a8b8-776a9cd8fd30:RS9ulf1-..."A token is a single string of the form njt:<id>:<secret>. Only a hash of the secret is stored, so a token is shown once, when it is created.
Permissions are not stored in the token. They are worked out from the user's access groups on every request, so changing a group or removing a user takes effect immediately.
Logging in
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "password": "..." }'The response holds the user and a session token:
{
"data": {
"id": "01a1aaf9-9469-731b-a4e1-de525d7171aa",
"name": "Admin",
"email": "[email protected]",
"groups": ["01a1aaf9-9468-75df-89bf-6bc71ea662ec"],
"createdAt": "2026-11-05T09:10:45.609Z",
"updatedAt": "2026-11-05T09:10:45.609Z",
"createdBy": null,
"token": "njt:01a1aaf9-94cd-7677-86e5-5c95a2a36737:sWCvU3yQ..."
}
}The password is never returned. A wrong email or password answers 401, and the response is the same for both, so it does not reveal which accounts exist.
After 10 failed attempts for the same email within 15 minutes, login answers 429 with "Too many failed attempts. Try again in a few minutes.".
Other auth endpoints
| Request | What it does |
|---|---|
GET /api/auth/check-token |
Returns the user the credential belongs to as { "data": user }. Use it to test a token |
DELETE /api/auth/logout |
Ends the session and clears its cookie. Sessions only |
GET /api/auth/permissions |
What the caller may do. See below |
What the caller may do
GET /api/auth/permissions lists each resource the caller has any access to, and the level for each action:
{
"data": [
{ "resource": "post", "scoped": true, "actions": { "read": "all", "create": "all", "update": "all", "delete": "all", "publish": "all" } },
{ "resource": "file", "scoped": true, "actions": { "read": "all", "create": "all", "delete": "all" } },
{ "resource": "analytics", "scoped": false, "actions": { "read": "all" } }
]
}A level is "all", "own" (only records the caller created) or "ref" (only each record's id and label). Any kind of token may call this, since it only tells the caller about itself. See Access control.
Session cookies
Login and setup also set an HttpOnly cookie named njin_session. A browser can use it instead of a header. This is how the admin panel works.
Because a cookie is sent automatically by the browser, a request that changes data (anything other than GET and HEAD) must also carry an X-Requested-With header with any value. Without it the request is answered with 403. Another website cannot add that header, which protects you from cross-site requests. A request that uses the Authorization header is not affected.
Which credential works where
| Endpoints | Session | Personal or OAuth token |
|---|---|---|
| Content: models, vars, files, analytics, schema | Yes | Yes, within its scopes |
| Users, access groups, tokens, connected apps | Yes | No. Answers 401 |
/mcp |
No | Yes |
A personal or OAuth token can never do more than the user it belongs to, and it can be narrowed to specific scopes. This is why a leaked agent token cannot create a replacement token for itself.
First-run setup
A new project has no users. GET /api/setup/status tells you whether setup is still needed:
{ "needsSetup": true }POST /api/setup creates the first admin and signs them in. The body is name, email and a password of at least 8 characters. The response is { "data": { "token": "...", "user": { ... } } }, and the session cookie is set.
The user is put in the built-in Full access group. Once any user exists, setup answers 403 with "Setup already completed".