KDB knowledge

Logins, tokens and roles over HTTP

How a program or the console proves who it is - the access token, HTTP Basic, POST /api/login - and the 401, 403 and 429 answers.

A server can be open, need a token, or need a user login. This page shows how to send each one over HTTP.

ServerLogin needed?
kdb serve on 127.0.0.1 (the default), no --token, no usersNo. Only programs on the same computer can connect.
started with --token <secret>Yes: the token. It may do everything.
the data folder has users (CREATE USER, kdb user add)Yes: a user name and password, or a login token. Each user may do only what their role allows. With --token too, the token also works.
--bind 0.0.0.0 (not 127.x) without --token and without usersthe server does not start

The examples below that need users or a token run on their own new kdb serve (a fresh data folder), so you see the real answers. On a new server without users, the first CREATE USER ... ROLE admin can be sent without a login; from then on every request needs one.

Ways to send a login

HowSendGood for
The access tokenheader Authorization: Bearer <token>a server started with --token
HTTP Basicheader Authorization: Basic <base64 of user:password> (curl -u user:password)scripts. A good password is remembered for 10 minutes, so it is not slow.
A login tokenPOST /api/login once, then Authorization: Bearer <token>apps that make many requests
An API tokenCREATE TOKEN app FOR USER u once, then Authorization: Bearer kdbt_... (API tokens)apps and services: many named tokens, each revoked on its own
The login cookiePOST /api/login sets the cookie kdb_loginbrowsers: the web console
The token cookiePOST /api/session turns a token into the cookie kdb_authbrowsers: the web console, when you paste a token

POST /api/login

Syntax:

POST /api/login
body: {"user": "<name>", "password": "<password>"[, "code": "<6 digits>"]}
   or user=<name>&password=<password>[&code=<6 digits>]  (Content-Type: application/x-www-form-urlencoded)

Checks a user name and password. On success it starts a login session (12 hours), sets the cookie kdb_login, and gives back a token for Authorization: Bearer <token>.

ParameterWhereMeaningDefault
userJSON body or form fieldThe user name (case is ignored)required
passwordJSON body or form fieldThe passwordrequired
codeJSON body or form fieldThe code of the authenticator app, for a user with two-factor login-
X-KDB-ConsoleheaderSent by the web console: then the answer has no token (the console uses only the cookie)not sent
Answer fieldMeaning
userThe user name as it is stored
roleThe user's own ROLE: admin, write, read or none (for every database)
grantsRoles on single databases, like {"shop": "write"}
expires_msWhen the login ends (ms since 1970, UTC)
tokenThe login token (43 characters). Send it as Authorization: Bearer <token>.
csrfThe X-KDB-CSRF value for changes sent with the cookie (CSRF)
header Set-Cookiekdb_login=<token>; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200 (and Secure over TLS, or when a proxy sends X-Forwarded-Proto: https)

HTTP: The first user, a login, the token, and logout

POST /exec?compat=0

CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin
###
GET /exec?compat=0&query=SELECT 1 AS one
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "boss-secret-1"}
###
GET /exec?compat=0&query=SELECT 1 AS one
Authorization: Bearer {{token}}
###
POST /api/logout
Authorization: Bearer {{token}}
###
GET /exec?compat=0&query=SELECT 1 AS one
Authorization: Bearer {{token}}

Response:

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin",
  "results": [
    {
      "statement": "CREATE USER boss WITH PASSWORD '***' ROLE admin",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'boss' created with role admin",
      "timings": {"parse_us": 0, "execute_us": 168626}
    }
  ],
  "timings": {"parse_us": 0, "execute_us": 168626}
}

GET /exec?compat=0&query=SELECT 1 AS one

HTTP 401 (application/json)

{
  "error": "login required: log in (POST /api/login), or send a user name and password (HTTP Basic)",
  "login": true
}

POST /api/login

HTTP 200 (application/json)

{
  "user": "boss",
  "role": "admin",
  "grants": {},
  "expires_ms": 1792691051375,
  "csrf": "6add673dff4486d3334de25d2d56f4eb",
  "token": "ddmM7HyYgI-RWGiA7sCk2US_DZikXzBnrjhDFkr2bQE"
}

GET /exec?compat=0&query=SELECT 1 AS one

HTTP 200 (application/json)

{
  "query": "SELECT 1 AS one",
  "results": [
    {
      "statement": "SELECT 1 AS one",
      "columns": [
        {"name": "one", "type": "LONG"}
      ],
      "dataset": [
        [1]
      ],
      "count": 1,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 23, "execute_us": 58}
    }
  ],
  "timings": {"parse_us": 23, "execute_us": 58}
}

POST /api/logout

HTTP 200 (application/json)

{"logged_out": true}

GET /exec?compat=0&query=SELECT 1 AS one

HTTP 401 (application/json)

{"error": "the login token is wrong or has expired; log in again (POST /api/login)", "login": true}

HTTP: Wrong passwords: 401, then 429 after 5 tries

POST /exec?compat=0

CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "guess-1"}
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "guess-2"}
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "guess-3"}
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "guess-4"}
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "guess-5"}
###
POST /api/login
Content-Type: application/json

{"user": "boss", "password": "boss-secret-1"}

Response:

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin",
  "results": [
    {
      "statement": "CREATE USER boss WITH PASSWORD '***' ROLE admin",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'boss' created with role admin",
      "timings": {"parse_us": 0, "execute_us": 163793}
    }
  ],
  "timings": {"parse_us": 0, "execute_us": 163793}
}

POST /api/login

HTTP 401 (application/json)

{"error": "wrong user name or password", "login": true}

POST /api/login

HTTP 401 (application/json)

{"error": "wrong user name or password", "login": true}

POST /api/login

HTTP 401 (application/json)

{"error": "wrong user name or password", "login": true}

POST /api/login

HTTP 401 (application/json)

{"error": "wrong user name or password", "login": true}

POST /api/login

HTTP 401 (application/json)

{"error": "wrong user name or password", "login": true}

POST /api/login

HTTP 429 (application/json)

{"error": "too many failed logins; try again in 1 s", "login": true}

The 429 answer also has the header Retry-After: 1. Even the right password must wait until then.

A login with curl -i shows the cookie (from chapter 16, real output of a server with the user shop_app):

curl -s -i -H "Content-Type: application/json" --data '{"user":"shop_app","password":"Another-long-secret-42"}' http://127.0.0.1:9900/api/login
HTTP/1.1 200 OK
Set-Cookie: kdb_login=472axoBncSsNLsieeFTU2Vz8Nomiuun3fYBWjd80u5E; Path=/; HttpOnly; SameSite=Strict; Max-Age=43200
{"user":"shop_app","role":"none","grants":{"shop":"write"},"expires_ms":1789188565554,"token":"472axoBncSsNLsieeFTU2Vz8Nomiuun3fYBWjd80u5E"}

On a server without users:

HTTP: A login on a server without users

POST /api/login
Content-Type: application/json

{"user": "asha", "password": "not-a-real-password"}

Response:

HTTP 400 (application/json)

{
  "error": "this server has no users yet; create one with CREATE USER <name> WITH PASSWORD '...' ROLE admin"
}

HTTP: No user name

POST /api/login
Content-Type: application/json

{"password": "secret"}

Response:

HTTP 400 (application/json)

{"error": "send the user name and password: {\"user\": \"...\", \"password\": \"...\"}"}

Log in once, then use the token (Python, requests):

import requests

KDB = "http://127.0.0.1:9900"
r = requests.post(f"{KDB}/api/login", json={"user": "shop_app", "password": "Another-long-secret-42"})
if r.status_code == 429:
    raise RuntimeError(f"too many failed logins: wait {r.headers['Retry-After']} s")
if r.status_code != 200:
    raise RuntimeError(r.json()["error"])
auth = {"Authorization": "Bearer " + r.json()["token"]}

rows = requests.post(f"{KDB}/exec", params={"db": "shop", "compat": 0},
                     data="SELECT count(*) FROM orders", headers=auth).json()
print(rows["results"][0]["dataset"])

requests.post(f"{KDB}/api/logout", headers=auth)
// Node 18+
const KDB = "http://127.0.0.1:9900";
const login = await fetch(`${KDB}/api/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ user: "shop_app", password: process.env.KDB_PASSWORD }),
});
const me = await login.json();
if (!login.ok) throw new Error(me.error);
const auth = { Authorization: `Bearer ${me.token}` };
const res = await fetch(`${KDB}/exec?db=shop&compat=0`, { method: "POST", headers: auth, body: "SELECT 1" });
console.log(res.status, (await res.json()).results);
$login = Invoke-RestMethod -Method Post "http://127.0.0.1:9900/api/login" -ContentType "application/json" `
  -Body (@{ user = "shop_app"; password = $env:KDB_PASSWORD } | ConvertTo-Json)
$auth = @{ Authorization = "Bearer $($login.token)" }
Invoke-RestMethod -Method Post "http://127.0.0.1:9900/exec?db=shop&compat=0" -Headers $auth -ContentType "text/plain" -Body "SELECT 1"
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:9900") };
var login = await http.PostAsJsonAsync("/api/login", new { user = "shop_app", password = Environment.GetEnvironmentVariable("KDB_PASSWORD") });
var body = await login.Content.ReadFromJsonAsync<JsonElement>();
if (!login.IsSuccessStatusCode) throw new Exception(body.GetProperty("error").GetString());
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", body.GetProperty("token").GetString());

POST /api/logout

Syntax:

POST /api/logout

Ends a login session: the one of the Authorization: Bearer <token> header, and the one of the kdb_login cookie. It also ends the console session of the kdb_auth cookie on the server, and tells the browser to forget the cookies kdb_login and kdb_auth.

ParameterWhereMeaningDefault
Authorization: Bearer <token>headerThe login token to end-
kdb_logincookieThe login cookie to end-
Answer fieldMeaning
logged_outalways true
header Set-Cookiekdb_login=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0 and the same for kdb_auth (removes the cookies)

HTTP: Log out (here without a login: still 200)

POST /api/logout

Response:

HTTP 200 (application/json)

{"logged_out": true}

GET /api/me

Syntax:

GET /api/me

Who am I? The user of this request's login, and the role. The web console shows it at the top ("boss (admin)").

ParameterWhereMeaningDefault
(a login)header or cookieAny of the ways to send a login-
Answer fieldMeaning
userThe user name, or null (the token, or a server without login)
roleThe user's own ROLE; admin for full access
grantsRoles on single databases: {"shop": "write"}
full_accesstrue for the --token, and on a server without login
two_factortrue when this user has two-factor login on
login_requiredtrue when requests need a login or token
userstrue when the data folder has users
cookietrue when this request logged in with a cookie
csrfOnly with a cookie login: the X-KDB-CSRF value for changes (CSRF)

HTTP: On a server without token and users: full access

GET /api/me

Response:

HTTP 200 (application/json)

{
  "user": null,
  "role": "admin",
  "grants": {},
  "full_access": true,
  "two_factor": false,
  "login_required": false,
  "users": false,
  "cookie": false,
  "step_up_actions": false,
  "step_up_minutes": 5,
  "step_up_until_ms": 0
}

HTTP: A user with a grant on one database

POST /exec?compat=0

CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin;
CREATE USER shop_app WITH PASSWORD 'Another-long-secret-42';
CREATE DATABASE shop;
GRANT write ON DATABASE shop TO shop_app
###
POST /api/login
Content-Type: application/json

{"user": "shop_app", "password": "Another-long-secret-42"}
###
GET /api/me
Authorization: Bearer {{token}}

Response:

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin;\nCREATE USER shop_app WITH PASSWORD 'Another-long-secret-42';\nCREATE DATABASE shop;\nGRANT write ON DATABASE shop TO shop_app",
  "results": [
    {
      "statement": "CREATE USER boss WITH PASSWORD '***' ROLE admin",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'boss' created with role admin",
      "timings": {"parse_us": 0, "execute_us": 162348}
    },
    {
      "statement": "CREATE USER shop_app WITH PASSWORD '***'",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'shop_app' created with role none",
      "timings": {"parse_us": 0, "execute_us": 60424}
    },
    {
      "statement": "CREATE DATABASE shop",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "database 'shop' created",
      "timings": {"parse_us": 0, "execute_us": 9231}
    },
    {
      "statement": "GRANT write ON DATABASE shop TO shop_app",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'shop_app' has role write on database 'shop'",
      "timings": {"parse_us": 0, "execute_us": 1761}
    }
  ],
  "timings": {"parse_us": 0, "execute_us": 233764}
}

POST /api/login

HTTP 200 (application/json)

{
  "user": "shop_app",
  "role": "none",
  "grants": {"shop": "write"},
  "expires_ms": 1792691051520,
  "csrf": "5993b92d7d2144c05a35e348320d66f3",
  "token": "DX6j_aIpvcyeyR62ZcpAJIjbbb62tzatkYwCMvnLlbI"
}

GET /api/me

HTTP 200 (application/json)

{
  "user": "shop_app",
  "role": "none",
  "grants": {"shop": "write"},
  "full_access": false,
  "two_factor": false,
  "login_required": true,
  "users": true,
  "cookie": false,
  "step_up_actions": false,
  "step_up_minutes": 5,
  "step_up_until_ms": 0
}

The access token (--token)

Syntax:

kdb serve --token <long random secret>
Authorization: Bearer <token>

The simplest protection: one secret for everything. Whoever sends it may do everything, like an admin.

SituationAnswer
no token sent401 {"error": "access token required"}
a wrong token401 {"error": "wrong access token"}
the right tokenthe normal answer

HTTP: A server started with --token change-me-please (shows an error)

GET /health
###
GET /exec?compat=0&query=SELECT 1 AS one
###
GET /exec?compat=0&query=SELECT 1 AS one
Authorization: Bearer wrong
###
GET /exec?compat=0&query=SELECT 1 AS one
Authorization: Bearer change-me-please

Response:

GET /health

HTTP 200 (application/json)

{
  "status": "ok",
  "version": "0.2.0-dev",
  "server_time_ms": 1792647851208,
  "auth_required": true,
  "login": false,
  "tls": false,
  "ready": true,
  "standby": false
}

GET /exec?compat=0&query=SELECT 1 AS one

HTTP 401 (application/json)

{"error": "access token required"}

GET /exec?compat=0&query=SELECT 1 AS one

HTTP 401 (application/json)

{"error": "wrong access token"}

GET /exec?compat=0&query=SELECT 1 AS one

HTTP 200 (application/json)

{
  "query": "SELECT 1 AS one",
  "results": [
    {
      "statement": "SELECT 1 AS one",
      "columns": [
        {"name": "one", "type": "LONG"}
      ],
      "dataset": [
        [1]
      ],
      "count": 1,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 35, "execute_us": 72}
    }
  ],
  "timings": {"parse_us": 35, "execute_us": 72}
}
curl -s -H "Authorization: Bearer change-me-please" "http://127.0.0.1:9900/exec?compat=0&query=SELECT%201"
$h = @{ Authorization = "Bearer $env:KDB_TOKEN" }
Invoke-RestMethod "http://127.0.0.1:9900/api/info" -Headers $h

POST /api/session

Syntax:

POST /api/session[?remember=1]
Authorization: Bearer <token>

Starts a console session for a token and gives the browser the cookie kdb_auth. The web console calls it when you paste a token. The server keeps the token in its memory; the cookie holds only a random session id. The cookie is HttpOnly, so no script in the page can read it.

ParameterWhereMeaningDefault
Authorization: Bearer <token>headerThe --token, an API token or a login tokenrequired
rememberquery1: the session lasts 30 days. Without it the session lasts 12 hours, and the browser forgets the cookie when it closes.not sent

A console session ends when:

What happensWhy
its time is up (12 hours, or 30 days with remember)a forgotten browser does not stay logged in
POST /api/logout with the cookiethe server forgets the id: a copied cookie stops working too
the pasted token stops working (DROP TOKEN, an ended login)the session has the token's rights, checked on every request
the server restartssessions live in memory only: paste the token again
Answer fieldMeaning
userThe token's user, or null for the --token
roleThe role; admin for the --token
full_accesstrue for the --token
remembertrue when the cookie is kept 30 days
csrfThe X-KDB-CSRF value for changes sent with the cookie (CSRF)
header Set-Cookiekdb_auth=<session id>; Path=/; HttpOnly; SameSite=Strict (with Max-Age=2592000 for remember, and Secure over TLS)

HTTP: The console hands over a token and gets a session cookie

POST /api/session
Authorization: Bearer change-me-please

Response:

HTTP 200 (application/json)

{
  "user": null,
  "role": "admin",
  "full_access": true,
  "remember": false,
  "csrf": "d940cd8cd42f3877ccaf73469a7f075f"
}

CSRF: the X-KDB-CSRF header

A browser sends cookies by itself, even when another web site makes it send a request. That is called CSRF (cross-site request forgery). So a request that logged in with a cookie and changes something must also send the header X-KDB-CSRF.

RequestNeeds X-KDB-CSRF?
GET or HEAD (only reads)no
POST, PUT, DELETE with the cookie kdb_login or kdb_authyes
any request with Authorization: Bearer or Basicno: another web site cannot set that header
the old cookie kdb_tokenno, but the request must come from the console (X-KDB-Console)

HTTP: A change with the cookie: refused without the header, done with it (shows an error)

POST /api/session
Authorization: Bearer change-me-please
###
POST /exec?compat=0
Cookie: kdb_auth=change-me-please

CREATE TABLE notes (id INT, text STRING)
###
POST /exec?compat=0
Cookie: kdb_auth=change-me-please
X-KDB-CSRF: {{csrf}}

CREATE TABLE notes (id INT, text STRING)

Response:

POST /api/session

HTTP 200 (application/json)

{
  "user": null,
  "role": "admin",
  "full_access": true,
  "remember": false,
  "csrf": "35b07c9baa23af37a447fc6924202791"
}

POST /exec?compat=0

HTTP 401 (application/json)

{"error": "the saved access token is no longer accepted; enter it again"}

POST /exec?compat=0

HTTP 401 (application/json)

{"error": "the saved access token is no longer accepted; enter it again"}

HTTP Basic

Syntax:

Authorization: Basic <base64 of name:password>
curl -u <name>:<password> ...

Sends the user name and password with every request. KDB checks the password once and then remembers it for 10 minutes, so it stays fast.

The header below is the base64 of reader:reader-secret-1, what curl -u reader:reader-secret-1 sends. The user reader has role read, so reading works and writing is refused:

HTTP: HTTP Basic with a read-only user (sample data: sample)

POST /exec?compat=0

CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin;
CREATE USER reader WITH PASSWORD 'reader-secret-1' ROLE read
###
GET /exec?compat=0&query=SELECT symbol, name FROM securities ORDER BY symbol LIMIT 2
Authorization: Basic cmVhZGVyOnJlYWRlci1zZWNyZXQtMQ==
###
POST /exec?compat=0
Authorization: Basic cmVhZGVyOnJlYWRlci1zZWNyZXQtMQ==

INSERT INTO brokers VALUES (99, 'Not Allowed Securities')

Response:

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "CREATE USER boss WITH PASSWORD 'boss-secret-1' ROLE admin;\nCREATE USER reader WITH PASSWORD 'reader-secret-1' ROLE read",
  "results": [
    {
      "statement": "CREATE USER boss WITH PASSWORD '***' ROLE admin",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'boss' created with role admin",
      "timings": {"parse_us": 0, "execute_us": 166112}
    },
    {
      "statement": "CREATE USER reader WITH PASSWORD '***' ROLE read",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "user 'reader' created with role read",
      "timings": {"parse_us": 0, "execute_us": 62273}
    }
  ],
  "timings": {"parse_us": 0, "execute_us": 228385}
}

GET /exec?compat=0&query=SELECT symbol, name FROM securities ORDER BY symbol LIMIT 2

HTTP 200 (application/json)

{
  "query": "SELECT symbol, name FROM securities ORDER BY symbol LIMIT 2",
  "results": [
    {
      "statement": "SELECT symbol, name FROM securities ORDER BY symbol LIMIT 2",
      "columns": [
        {"name": "symbol", "type": "SYMBOL"},
        {"name": "name", "type": "STRING"}
      ],
      "dataset": [
        ["ACME", "Acme Investment Company"],
        ["CITY", "City Bank Limited"]
      ],
      "count": 2,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 23, "execute_us": 245}
    }
  ],
  "timings": {"parse_us": 23, "execute_us": 245}
}

POST /exec?compat=0

HTTP 403 (application/json)

{
  "query": "INSERT INTO brokers VALUES (99, 'Not Allowed Securities')",
  "results": [],
  "saved": 0,
  "rolled_back": false,
  "error": "INSERT needs role write on database 'demo'; user 'reader' has read",
  "sqlstate": "42501"
}
curl -s -u reader:reader-secret-1 "http://127.0.0.1:9900/exec?db=market&compat=0&query=SELECT%20count(*)%20FROM%20ochl_day"
import requests
r = requests.get("http://127.0.0.1:9900/exec", params={"db": "shop", "query": "SELECT count(*) FROM app_users"},
                 auth=("report", "Reports-only-secret-7"))

Roles: what each route needs

Each user has a role: none, read, write or admin. The user's own ROLE counts for every database; a GRANT ... ON DATABASE gives a role on one database. KDB uses the stronger of the two.

RouteNeeds
GET /health, POST /api/login, POST /api/logout, the console filesnothing
GET /api/me, POST /session, DELETE /session/<id>, POST /api/feedbackany login
/exec and /exp with SQL that reads, BEGIN/COMMIT/ROLLBACKread on the database
GET /api/info, GET /api/disk, GET /api/backups, GET /api/trigger-queue, GET /api/stats, GET /api/timers, GET /api/pins, /cursorread
/exec with INSERT, UPDATE, DELETE, CALL; POST /write, POST /imp into a table that is therewrite
/exec with CREATE/ALTER/DROP of tables, views, functions, procedures, triggers, indexes; BACKUP DATABASE, CHECK TABLE; POST /api/check, POST /api/backup; a new table with POST /imp; saving, running and removing timers; pinning and unpinning statementsadmin on the database
POST /api/databases, DELETE /api/databases/<name>, GET /api/settings, POST /api/settings, POST /api/restart, GET /api/audit, GET /api/feedback, POST /api/feedback/<id>, CREATE USER, GRANT, ...the user's own ROLE admin
GET /api/databasesany login; lists only the databases the user may read

401, 403 and 429 answers

AnswerMeansWhat to do
401 {"error": "login required: log in (POST /api/login), or send a user name and password (HTTP Basic)", "login": true}no login on a server with userslog in
401 {"error": "wrong user name or password", "login": true}a wrong name or passwordcheck them
401 {"error": "the login token is wrong or has expired; log in again (POST /api/login)"}an old or wrong login tokenlog in again
401 {"error": "your login has expired; log in again"}an old login cookielog in again
401 {"error": "access token required"} / {"error": "wrong access token"}a server with --tokensend the right token
403 {"error": "INSERT needs role write on database 'shop'; user 'report' has read"}logged in, but the role is too weakuse another user, or GRANT
403 {"error": "cross-site requests are not allowed"}a browser request from another sitecall KDB from your backend
403 {"error": "this change came with a login cookie but without the right X-KDB-CSRF header; ...", "csrf": true}a change sent with a cookie but no X-KDB-CSRFsend the csrf value, see CSRF
429 {"error": "too many failed logins; try again in 4 s", "login": true} and Retry-After: 4too many failed loginswait that many seconds

The 403 role message is shown for real in HTTP Basic. In a 401 answer, "login": true tells a program that the server has users (so it should log in, not ask for a token).

HTTP: A request from another web site is refused before any login check

POST /api/login
Content-Type: application/json
Origin: http://evil.example

{"user": "boss", "password": "boss-secret-1"}

Response:

HTTP 403 (application/json)

{"error": "cross-origin requests are not allowed"}