Skip to main content

HTTP API

CamusDB exposes JSON endpoints for automation and application integration. Request and response properties use camelCase.

For HTTP/2 and Protobuf-based clients, see gRPC API. The REST/JSON and gRPC APIs reach the same SQL, transaction, and row-operation engine.

Status And Errors

Successful responses use:

{
"status": "ok"
}

Failed responses include a CamusDB error code when available. Many domain errors map to HTTP 500, while authentication, authorization, validation, not found, conflict, and rate-limit errors use their matching HTTP status:

{
"status": "failed",
"code": "CADB0400",
"message": "error message"
}

See Error Codes for the reference list and when each code is generated.

Authentication

When authentication is enabled, use /login to obtain a bearer token:

POST /login
Content-Type: application/json

{ "user": "admin", "password": "secret" }

Successful response:

{
"status": "ok",
"token": "camus_<id>.<secret>",
"expiresAtUnixMs": 1785270000000,
"expiresInSeconds": 900
}

Send the token on later requests:

Authorization: Bearer camus_<id>.<secret>

Use /logout with the same header to revoke the current token.

expiresAtUnixMs is the absolute UTC token deadline. expiresInSeconds is the same deadline as a server-measured duration, useful for clients that renew on a timer instead of trusting local wall-clock time.

See Authentication And Authorization for environment variables, SQL user/grant statements, TLS requirements, and privilege enforcement.

Column Values

Rows, filters, inserts, updates, defaults, and SQL parameters use ColumnValue objects:

ColumnType is serialized as its numeric enum value:

ValueType
0null
1id / object id
2int64
3string
4bool
5float64
6float32
7bytes
8date
9datetime
10array
11uuid
{ "type": 3, "strValue": "R2-D2", "longValue": 0, "floatValue": 0, "boolValue": false }
{ "type": 2, "strValue": null, "longValue": 1977, "floatValue": 0, "boolValue": false }
{ "type": 5, "strValue": null, "longValue": 0, "floatValue": 12.5, "boolValue": false }
{ "type": 6, "strValue": null, "longValue": 0, "floatValue": 12.5, "boolValue": false }
{ "type": 4, "strValue": null, "longValue": 0, "floatValue": 0, "boolValue": true }
{ "type": 1, "strValue": "507f1f77bcf86cd799439011", "longValue": 0, "floatValue": 0, "boolValue": false }
{ "type": 7, "bytesValue": "3q2+7w==", "longValue": 0, "floatValue": 0, "boolValue": false }
{ "type": 8, "longValue": 639039456000000000, "isoValue": "2026-03-15" }
{ "type": 9, "longValue": 639039888000000000, "isoValue": "2026-03-15T12:00:00.0000000Z" }
{ "type": 10, "arrayElementType": 2, "arrayValues": [{ "type": 2, "longValue": 42 }] }
{ "type": 11, "strValue": "550e8400-e29b-41d4-a716-446655440000", "longValue": 0, "uuidHigh": 0 }

For bytes, SQL literals use X'...' hexadecimal while JSON uses base64 in bytesValue. For date and datetime values, responses include isoValue; the stored value is represented by UTC ticks in longValue. For UUID request values, pass canonical hyphenated or 32-character hexadecimal text in strValue; responses include uuidValue for readability.

Health

GET /ping

Returns server status and UTC time.

{
"status": "ok",
"dateTime": "2026-05-28T18:30:00.0000000Z"
}

Databases

Databases must be created explicitly before table DDL, DML, or queries can use their name.

POST /create-db

{
"databaseName": "app",
"ifNotExists": true
}

POST /drop-db

{
"databaseName": "app"
}

The direct endpoint drops an existing database. For idempotent drops, use SQL:

{
"sql": "DROP DATABASE IF EXISTS app"
}

Database rename is also exposed through SQL:

{
"sql": "RENAME DATABASE app TO app_prod"
}

The equivalent ALTER DATABASE ... RENAME TO form is also accepted:

{
"sql": "ALTER DATABASE app RENAME TO app_prod"
}

POST /close-db

{
"databaseName": "app"
}

Tables

POST /create-table

{
"databaseName": "app",
"tableName": "robots",
"ifNotExists": true,
"columns": [
{ "name": "id", "type": "id", "notNull": true, "defaultValue": null },
{ "name": "name", "type": "string", "maxLength": 64, "notNull": true, "defaultValue": null },
{ "name": "payload", "type": "bytes", "notNull": false, "defaultValue": null },
{ "name": "tags", "type": "array", "arrayElementType": "string", "notNull": false, "defaultValue": null },
{
"name": "year",
"type": "int64",
"notNull": false,
"defaultValue": {
"type": 2,
"strValue": null,
"longValue": 2024,
"floatValue": 0,
"boolValue": false
}
}
]
}

The HTTP table-creation model accepts id, int64, float64, float32, bool, string, date, datetime, bytes, uuid, guid, and array. Use maxLength for string and bytes limits, and arrayElementType for array columns.

See Data Types for the SQL names, aliases, literal formats, and JSON value rules.

SQL Execution

Use the SQL endpoints when possible. They exercise the same parser and executor used by the engine tests.

Autocommit SQL requests use Serializable isolation by default. For requests that start an autocommit transaction, isolationLevel can be set to "Serializable" or "ReadCommitted", and transactionMode can be set to "ReadWrite" or "ReadOnly". Writable autocommit requests can also set locking to "Pessimistic" or "Optimistic". These fields are ignored when the request resumes an existing transaction with txnIdPT and txnIdCounter. locking is also ignored by read-only /execute-sql-query requests because they do not run a writable transaction that can acquire or validate write conflicts.

POST /execute-sql-ddl

For schema-changing SQL:

{
"databaseName": "app",
"sql": "CREATE TABLE IF NOT EXISTS robots (id OID PRIMARY KEY NOT NULL, name STRING NOT NULL, year INT64)",
"parameters": null
}

Server-level database statements can omit databaseName:

{
"sql": "CREATE DATABASE IF NOT EXISTS app",
"parameters": null
}

Operational server-level query statements such as SHOW ENGINE STATS can also omit databaseName.

POST /execute-sql-query

For SELECT and SHOW statements:

{
"databaseName": "app",
"sql": "SELECT id, name FROM robots WHERE year >= @year ORDER BY name ASC",
"isolationLevel": "Serializable",
"transactionMode": "ReadOnly",
"parameters": {
"@year": {
"type": 2,
"strValue": null,
"longValue": 1970,
"floatValue": 0,
"boolValue": false
}
}
}

Response:

{
"status": "ok",
"total": 1,
"rows": [
{
"id": { "type": 1, "strValue": "507f1f77bcf86cd799439011", "longValue": 0, "floatValue": 0, "boolValue": false },
"name": { "type": 3, "strValue": "R2-D2", "longValue": 0, "floatValue": 0, "boolValue": false }
}
]
}

Time-travel reads with AS OF SYSTEM TIME are supported through this endpoint for autocommit read-only SELECT statements:

{
"databaseName": "app",
"sql": "SELECT id, name FROM robots AS OF SYSTEM TIME '-10s' WHERE year >= @year",
"transactionMode": "ReadOnly",
"parameters": {
"@year": {
"type": 2,
"longValue": 1970
}
}
}

The same SQL restrictions apply over HTTP: the clause cannot run inside an explicit transaction and invalid time-travel values return CADB0409 InvalidAsOfSystemTime. See Time-Travel Reads.

POST /execute-sql-query-stream

For large SELECT or SHOW result sets, use the streaming endpoint:

{
"databaseName": "app",
"sql": "SELECT id, name FROM robots ORDER BY name",
"isolationLevel": "Serializable",
"transactionMode": "ReadOnly",
"parameters": null
}

The request body is the same shape as /execute-sql-query. The response uses newline-delimited JSON with content type application/x-ndjson:

{"status":"ok","columns":[{"name":"id","type":1},{"name":"name","type":3}]}
["507f1f77bcf86cd799439011","R2-D2"]
["507f1f77bcf86cd799439012","C-3PO"]
{"status":"ok","total":2,"serverTimeMs":3.1}

The first line is the schema header. Each row is a compact positional array aligned to the header's column order. The final line is a trailer with the terminal status, total rows streamed, optional causal token, and server time.

If an error happens before the first line is written, CamusDB returns a normal JSON error body with the mapped HTTP status. If an error happens after the stream has started, the HTTP status may already be 200, so CamusDB reports the failure in the final trailer:

{"status":"failed","total":128,"code":"CADB0502","message":"transaction conflict","serverTimeMs":12.4}

Autocommit streaming runs as a single attempt. Because rows may already be on the wire before commit, the server cannot transparently replay a late Serializable conflict the way the buffered endpoint can. Use /execute-sql-query when automatic autocommit retry is more important than incremental delivery, or use an explicit transaction and retry client-side.

POST /execute-sql-non-query

For INSERT, UPDATE, and DELETE statements:

{
"databaseName": "app",
"sql": "UPDATE robots SET name = @name WHERE id = @id",
"parameters": {
"@name": { "type": 3, "strValue": "Artoo", "longValue": 0, "floatValue": 0, "boolValue": false },
"@id": { "type": 1, "strValue": "507f1f77bcf86cd799439011", "longValue": 0, "floatValue": 0, "boolValue": false }
}
}

Response:

{
"status": "ok",
"rows": 1
}

Prepared Statements

Prepared statements let a client register a SQL statement once, then execute it many times by handle with different positional values. They are useful for hot parameterized SELECT, INSERT, UPDATE, DELETE, and SHOW statements.

Register a statement:

POST /prepare-sql-statement
Content-Type: application/json

{
"databaseName": "app",
"sql": "SELECT id, name FROM robots WHERE year >= @year"
}

Response:

{
"status": "ok",
"statementId": "opaque-node-local-handle",
"parameterNames": ["@year"]
}

Execute it through /execute-sql-query, /execute-sql-query-stream, or /execute-sql-non-query:

{
"statementId": "opaque-node-local-handle",
"positionalParameters": [
{ "type": 2, "longValue": 1980 }
],
"transactionMode": "ReadOnly"
}

When statementId is present, omit sql, databaseName, and named parameters. positionalParameters[i] binds to parameterNames[i] from the prepare response.

Close a handle:

POST /close-sql-statement
Content-Type: application/json

{ "statementId": "opaque-node-local-handle" }

REST handles are scoped to the node and principal that prepared them. If an execution returns CADB0520 UnknownPreparedStatement, prepare again and replay once. See Prepared Statements for handle scope, authorization behavior, and limits.

Direct Row Operations

Direct endpoints accept filters instead of SQL strings. Filters contain a column name, an operator, and a ColumnValue.

OrderType is also numeric: 0 ascending and 1 descending.

{
"columnName": "year",
"op": ">=",
"value": {
"type": 2,
"strValue": null,
"longValue": 1970,
"floatValue": 0,
"boolValue": false
}
}

POST /insert

{
"databaseName": "app",
"tableName": "robots",
"values": {
"id": { "type": 1, "strValue": "507f1f77bcf86cd799439011", "longValue": 0, "floatValue": 0, "boolValue": false },
"name": { "type": 3, "strValue": "R2-D2", "longValue": 0, "floatValue": 0, "boolValue": false },
"year": { "type": 2, "strValue": null, "longValue": 1977, "floatValue": 0, "boolValue": false }
}
}

POST /query

{
"databaseName": "app",
"tableName": "robots",
"filters": [
{
"columnName": "year",
"op": ">=",
"value": { "type": 2, "strValue": null, "longValue": 1970, "floatValue": 0, "boolValue": false }
}
],
"orderBy": [
{ "columnName": "year", "type": 1 }
]
}

POST /query-by-id

{
"databaseName": "app",
"tableName": "robots",
"id": "507f1f77bcf86cd799439011"
}

POST /update

{
"databaseName": "app",
"tableName": "robots",
"values": {
"name": { "type": 3, "strValue": "Artoo", "longValue": 0, "floatValue": 0, "boolValue": false }
},
"filters": [
{
"columnName": "id",
"op": "=",
"value": { "type": 1, "strValue": "507f1f77bcf86cd799439011", "longValue": 0, "floatValue": 0, "boolValue": false }
}
]
}

POST /delete

{
"databaseName": "app",
"tableName": "robots",
"filters": [
{
"columnName": "year",
"op": "<",
"value": { "type": 2, "strValue": null, "longValue": 1970, "floatValue": 0, "boolValue": false }
}
]
}

Explicit Transactions

Start a transaction:

POST /start-transaction

{
"databaseName": "app",
"isolationLevel": "Serializable",
"transactionMode": "ReadWrite",
"locking": "Pessimistic"
}

isolationLevel, transactionMode, and locking are optional. If omitted, the transaction starts with the server default isolation level, which is Serializable, the default transaction mode, which is read-write, and the server default locking strategy, which is pessimistic.

Use "ReadCommitted" only when you intentionally opt down from the default Serializable behavior. Use "ReadOnly" with "Serializable" for a stable snapshot transaction. Use "Optimistic" when you want conflicts detected at commit instead of taking explicit locks while the transaction runs. Unrecognized locking values are rejected with InvalidInput.

Response:

{
"status": "ok",
"txnIdPT": 123,
"txnIdCounter": 1
}

Pass txnIdPT and txnIdCounter to subsequent SQL or direct row requests to reuse that transaction:

{
"databaseName": "app",
"txnIdPT": 123,
"txnIdCounter": 1,
"sql": "INSERT INTO robots (id, name) VALUES (GEN_ID(), \"K-2SO\")"
}

Commit or roll back:

POST /commit-transaction

{
"databaseName": "app",
"txnIdPT": 123,
"txnIdCounter": 1
}

POST /rollback-transaction

{
"databaseName": "app",
"txnIdPT": 123,
"txnIdCounter": 1
}

If COMMIT or ROLLBACK returns CADB0509 TransactionFinalizeUnresolved, retry the same request with the same transaction id. Do not start a fresh transaction and replay the statements, because the original commit may already have succeeded server-side.