KDB knowledge

Saved queries as URLs (CREATE ENDPOINT)

Save a SELECT with named parameters under a name; programs then call GET /v/<name>?param=value and get JSON, without sending SQL.

An endpoint is a saved SELECT with parameters. A program calls it with a plain URL and gets the same JSON as /exec, without writing or sending any SQL:

GET /v/last_close?symbol=CITY&n=5

Why: the SQL lives in one place (the database), a client cannot change it, and each value from the URL is checked and used as a value, never as SQL text, so nobody can slip SQL in through a parameter.

CREATE ENDPOINT

Syntax:

CREATE [OR REPLACE] ENDPOINT [GET] /v/name [(param TYPE [DEFAULT value], ...)] [ROLE read | write | ddl | admin]
  AS SELECT ... :param ...
PartMeaning
/v/namethe URL: GET /v/name. Letters, digits and _. Writing GET /v/ is optional: CREATE ENDPOINT last_close (...) is the same
(param TYPE ...)the parameters, with a type each: INT, LONG, DOUBLE, BOOL, TEXT, SYMBOL, TIMESTAMP, DATE ...
DEFAULT valueused when the URL leaves the parameter out; a constant (10, 'NEPSE', true)
ROLEthe least role a caller needs on the database to call it; read when not given
AS SELECTone query; :param is where a parameter's value goes ($1, $2 ... by position work too)

HTTP: Save the last closes of a symbol as an endpoint, then call it (sample data: sample)

POST /exec?compat=0

CREATE OR REPLACE ENDPOINT GET /v/last_close(symbol TEXT, n INT DEFAULT 3) AS
  SELECT time, close FROM ochl_day WHERE symbol = :symbol ORDER BY time DESC LIMIT :n
###
GET /v/last_close?symbol=CITY
###
GET /v/last_close?symbol=CITY&n=1

Response:

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "CREATE OR REPLACE ENDPOINT GET /v/last_close(symbol TEXT, n INT DEFAULT 3) AS\n  SELECT time, close FROM ochl_day WHERE symbol = :symbol ORDER BY time DESC LIMIT :n",
  "results": [
    {
      "statement": "CREATE OR REPLACE ENDPOINT GET /v/last_close(symbol STRING, n INT DEFAULT 3) ROLE read AS SELECT time , close FROM ochl_day WHERE symbol = $1 ORDER BY time DESC LIMIT $2",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "endpoint 'last_close' created: GET /v/last_close",
      "timings": {"parse_us": 30, "execute_us": 23089}
    }
  ],
  "timings": {"parse_us": 30, "execute_us": 23089}
}

GET /v/last_close?symbol=CITY

HTTP 200 (application/json)

{
  "query": "SELECT time , close FROM ochl_day WHERE symbol = $1 ORDER BY time DESC LIMIT $2",
  "results": [
    {
      "statement": "SELECT time, close FROM ochl_day WHERE symbol = 'CITY' ORDER BY time DESC LIMIT 3",
      "columns": [
        {"name": "time", "type": "TIMESTAMP"},
        {"name": "close", "type": "DOUBLE"}
      ],
      "dataset": [
        ["2026-09-10T00:00:00.000Z", 504.9],
        ["2026-09-09T00:00:00.000Z", 504.6],
        ["2026-09-08T00:00:00.000Z", 497.5]
      ],
      "count": 3,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 11, "execute_us": 285}
    }
  ],
  "columns": [
    {"name": "time", "type": "TIMESTAMP"},
    {"name": "close", "type": "DOUBLE"}
  ],
  "dataset": [
    ["2026-09-10T00:00:00.000Z", 504.9],
    ["2026-09-09T00:00:00.000Z", 504.6],
    ["2026-09-08T00:00:00.000Z", 497.5]
  ],
  "count": 3,
  "timings": {"parse_us": 11, "execute_us": 285}
}

GET /v/last_close?symbol=CITY&n=1

HTTP 200 (application/json)

{
  "query": "SELECT time , close FROM ochl_day WHERE symbol = $1 ORDER BY time DESC LIMIT $2",
  "results": [
    {
      "statement": "SELECT time, close FROM ochl_day WHERE symbol = 'CITY' ORDER BY time DESC LIMIT 1",
      "columns": [
        {"name": "time", "type": "TIMESTAMP"},
        {"name": "close", "type": "DOUBLE"}
      ],
      "dataset": [
        ["2026-09-10T00:00:00.000Z", 504.9]
      ],
      "count": 1,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 9, "execute_us": 65}
    }
  ],
  "columns": [
    {"name": "time", "type": "TIMESTAMP"},
    {"name": "close", "type": "DOUBLE"}
  ],
  "dataset": [
    ["2026-09-10T00:00:00.000Z", 504.9]
  ],
  "count": 1,
  "timings": {"parse_us": 9, "execute_us": 65}
}

Calling an endpoint

Syntax:

GET /v/name?param=value&...[&db=<database>][&format=columns][&limit=<n>]
AnswerWhen
200 and the JSON of /execit ran
400a parameter is missing (and has no DEFAULT), is not of its type (n=abc for an INT), or is not a parameter of the endpoint. The message shows how to call it
403the caller's role on the database is below the endpoint's ROLE
404no endpoint of that name in the database

HTTP: A wrong parameter is a 400 that says how to call it (sample data: sample)

POST /exec?compat=0

CREATE OR REPLACE ENDPOINT GET /v/closes(symbol TEXT, n INT) AS
  SELECT close FROM ochl_day WHERE symbol = :symbol ORDER BY time DESC LIMIT :n
###
GET /v/closes?symbol=CITY&n=three

Response:

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "CREATE OR REPLACE ENDPOINT GET /v/closes(symbol TEXT, n INT) AS\n  SELECT close FROM ochl_day WHERE symbol = :symbol ORDER BY time DESC LIMIT :n",
  "results": [
    {
      "statement": "CREATE OR REPLACE ENDPOINT GET /v/closes(symbol STRING, n INT) ROLE read AS SELECT close FROM ochl_day WHERE symbol = $1 ORDER BY time DESC LIMIT $2",
      "columns": [],
      "dataset": [],
      "count": 0,
      "truncated": false,
      "rows_affected": 0,
      "message": "endpoint 'closes' created: GET /v/closes",
      "timings": {"parse_us": 16, "execute_us": 23115}
    }
  ],
  "timings": {"parse_us": 16, "execute_us": 23115}
}

GET /v/closes?symbol=CITY&n=three

HTTP 400 (application/json)

{"error": "parameter 'n': 'three' is not a whole number"}

DROP ENDPOINT

Syntax:

DROP ENDPOINT [IF EXISTS] name

The URL answers 404 from then on.

How it is stored

An endpoint is kept with the database's functions and procedures, so a backup, a restore and a restart keep it. A database with an endpoint cannot be opened by a KDB built before 2026-10-03; it says it needs a newer KDB and changes nothing.