Skip to content

ZeptoDB HTTP API Reference

Last updated: 2026-03-25

The HTTP server (port 8123) is ClickHouse-compatible. Grafana can connect directly using the ClickHouse data source plugin with no modification.


Enterprise security guide: Security Operations Guide · SSO Integration Guide


Terminal window
# Build (see README for full build instructions)
cd build && ninja -j$(nproc)
# Start with default settings (port 8123, no auth)
./zepto_server --port 8123
# Start with TLS + auth enabled
./zepto_server --port 8123 --tls-cert server.crt --tls-key server.key
Terminal window
# Health check
curl http://localhost:8123/ping
# Ok
# Simple aggregation (string symbol)
curl -s -X POST http://localhost:8123/ \
-d "SELECT vwap(price, volume) AS vwap, count(*) AS n FROM trades WHERE symbol = 'AAPL'"
# {"columns":["vwap","n"],"data":[[15037.2,1000]],"rows":1,"execution_time_us":52.3}
# Integer symbol ID also supported
curl -s -X POST http://localhost:8123/ \
-d 'SELECT vwap(price, volume) AS vwap, count(*) AS n FROM trades WHERE symbol = 1'
Terminal window
# 5-minute OHLCV bars
curl -s -X POST http://localhost:8123/ -d '
SELECT xbar(timestamp, 300000000000) AS bar,
first(price) AS open, max(price) AS high,
min(price) AS low, last(price) AS close, sum(volume) AS vol
FROM trades WHERE symbol = 1
GROUP BY xbar(timestamp, 300000000000) ORDER BY bar ASC
' | python3 -m json.tool
# Volume by symbol
curl -s -X POST http://localhost:8123/ \
-d 'SELECT symbol, sum(volume) AS total_vol FROM trades GROUP BY symbol ORDER BY symbol'
# Last 10 minutes of trades for symbol 1
curl -s -X POST http://localhost:8123/ \
-d "SELECT price, volume, epoch_s(timestamp) AS ts FROM trades
WHERE symbol = 1 AND timestamp > $(date +%s)000000000 - 600000000000
ORDER BY timestamp DESC LIMIT 100"
Terminal window
# Generate and store an API key (admin role, server-side tool)
./zepto_server --gen-key --role admin
# zepto_a1b2c3d4e5f6... (64 hex chars)
# Use the key
export APEX_KEY="zepto_a1b2c3d4e5f6..."
curl -s -X POST http://localhost:8123/ \
-H "Authorization: Bearer $APEX_KEY" \
-d 'SELECT count(*) FROM trades'
import zepto_py as apex
db = zeptodb.connect("localhost", 8123)
# DataFrame results
df = db.query_pandas("SELECT symbol, avg(price) FROM trades GROUP BY symbol")
print(df)

MethodPathAuth requiredDescription
POST/yesExecute SQL query
GET/yesExecute SQL via ?query= param
GET/pingnoHealth check — returns "Ok\n"
GET/healthnoKubernetes liveness probe
GET/readynoKubernetes readiness probe
GET/whoamiyesReturn authenticated role and subject
GET/statsyesPipeline statistics (JSON)
GET/metricsnoPrometheus OpenMetrics
GET/admin/keysadminList API keys
POST/admin/keysadminCreate API key
DELETE/admin/keys/:idadminRevoke API key
GET/admin/queriesadminList running queries
DELETE/admin/queries/:idadminKill a running query
GET/admin/auditadminAudit log (last N events)
GET/admin/sessionsadminActive client sessions
GET/admin/versionadminServer version info
GET/admin/nodesadminCluster node status
POST/admin/nodesadminAdd remote node to cluster
DELETE/admin/nodes/:idadminRemove node from cluster
GET/admin/clusteradminCluster overview
GET/admin/metrics/historyadminMetrics time-series history
GET/admin/rebalance/statusadminCurrent rebalance status
POST/admin/rebalance/startadminStart rebalance (add/remove node)
POST/admin/rebalance/pauseadminPause current rebalance
POST/admin/rebalance/resumeadminResume paused rebalance
POST/admin/rebalance/canceladminCancel current rebalance
GET/admin/rebalance/historyadminPast rebalance events (up to 50, most recent last)

Public paths (/ping, /health, /ready) are always exempt from authentication.

Every response includes an X-Request-Id header for tracing (e.g., X-Request-Id: r0001a3).


Send a SQL string as the request body. Content-Type is not required.

Terminal window
curl -X POST http://localhost:8123/ \
-d 'SELECT vwap(price, volume), count(*) FROM trades WHERE symbol = 1'
Terminal window
# GROUP BY query
curl -X POST http://localhost:8123/ \
-d 'SELECT symbol, sum(volume) AS vol FROM trades GROUP BY symbol ORDER BY symbol'
Terminal window
# Multi-line SQL (use single quotes or heredoc)
curl -X POST http://localhost:8123/ -d '
SELECT xbar(timestamp, 300000000000) AS bar,
first(price) AS open, max(price) AS high,
min(price) AS low, last(price) AS close,
sum(volume) AS volume
FROM trades WHERE symbol = 1
GROUP BY xbar(timestamp, 300000000000)
ORDER BY bar ASC
'

All responses are JSON.

{
"columns": ["vwap(price, volume)", "count(*)"],
"data": [[15037.2, 1000]],
"rows": 1,
"execution_time_us": 52.3
}
FieldTypeDescription
columnsstring[]Column names in SELECT order
dataint64[][]Row-major result data. All values are int64.
rowsintNumber of result rows
execution_time_usfloatQuery execution time in microseconds

Note: Prices and timestamps are int64 in the response. Apply your scale factor client-side (e.g. divide by 100 for cents-to-dollars).

{
"columns": ["symbol", "vol"],
"data": [[1, 104500], [2, 101000]],
"rows": 2,
"execution_time_us": 88.1
}

When APEX_TLS_ENABLED is compiled in, the server requires authentication on all non-public paths.

Format: zepto_ followed by 64 hex characters (256-bit entropy).

Terminal window
curl -X POST http://localhost:8123/ \
-H "Authorization: Bearer zepto_a1b2c3d4...64hexchars" \
-d 'SELECT count(*) FROM trades'
Terminal window
curl -X POST http://localhost:8123/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d 'SELECT count(*) FROM trades'

JWT claims used:

  • sub — subject (user identifier)
  • role — APEX role string: admin, writer, reader, analyst, metrics
  • exp — expiration timestamp

If the Authorization header starts with ey, JWT validation is tried first. If JWT fails, API key validation is attempted as fallback.


Returns pipeline operational statistics as JSON.

Terminal window
curl http://localhost:8123/stats \
-H "Authorization: Bearer zepto_..."
{
"ticks_ingested": 5000000,
"ticks_stored": 4999800,
"ticks_dropped": 200,
"queries_executed": 12345,
"total_rows_scanned": 50000000,
"partitions_created": 10,
"last_ingest_latency_ns": 181
}
FieldDescription
ticks_ingestedTotal ticks pushed into the ring buffer
ticks_storedTicks successfully written to column store
ticks_droppedTicks dropped due to ring buffer overflow
queries_executedTotal SQL queries executed
total_rows_scannedCumulative rows scanned across all queries
partitions_createdNumber of partitions allocated
last_ingest_latency_nsLatency of the most recent ingest (nanoseconds)

Returns OpenMetrics-format text for Prometheus scraping.

Terminal window
curl http://localhost:8123/metrics
# HELP zepto_ticks_ingested_total Total ticks ingested
# TYPE zepto_ticks_ingested_total counter
zepto_ticks_ingested_total 5000000
# HELP zepto_ticks_stored_total Total ticks stored to column store
# TYPE zepto_ticks_stored_total counter
zepto_ticks_stored_total 4999800
# HELP zepto_ticks_dropped_total Total ticks dropped (queue overflow)
# TYPE zepto_ticks_dropped_total counter
zepto_ticks_dropped_total 200
# HELP zepto_queries_executed_total Total SQL queries executed
# TYPE zepto_queries_executed_total counter
zepto_queries_executed_total 12345
# HELP zepto_rows_scanned_total Total rows scanned
# TYPE zepto_rows_scanned_total counter
zepto_rows_scanned_total 50000000
# HELP zepto_partitions_total Total partitions created
# TYPE zepto_partitions_total gauge
zepto_partitions_total 10
# HELP zepto_last_ingest_latency_ns Last ingest latency in nanoseconds
# TYPE zepto_last_ingest_latency_ns gauge
zepto_last_ingest_latency_ns 181
  1. Add a ClickHouse data source in Grafana
  2. Host: localhost, Port: 8123
  3. Protocol: HTTP (or HTTPS with TLS)
  4. No database required — APEX uses trades and quotes table names directly in SQL

{
"columns": [],
"data": [],
"rows": 0,
"execution_time_us": 0,
"error": "Parse error: unexpected token 'FORM' at position 7"
}

Common error strings:

ErrorCause
"Parse error: ..."SQL syntax error
"Unknown table: foo"Table not found
"Query cancelled"Cancelled via CancellationToken
"Unauthorized"Missing or invalid credentials
"Forbidden"Valid credentials but insufficient role

HTTP status codes: 200 on success (even for SQL errors — check error field), 401 for auth failure, 403 for permission denied, 408 for query timeout/cancelled.


All admin endpoints require the admin role. Returns 401 without credentials, 403 with non-admin credentials.

Terminal window
curl http://localhost:8123/admin/keys -H "Authorization: Bearer $ADMIN_KEY"
[
{
"id": "ak_7f3k8a2b",
"name": "trading-desk-1",
"role": "writer",
"enabled": true,
"created_at_ns": 1711234567000000000,
"last_used_ns": 1711234590000000000,
"expires_at_ns": 0,
"tenant_id": "hft_desk_1",
"allowed_symbols": ["AAPL", "MSFT"],
"allowed_tables": ["trades", "quotes"]
}
]
Terminal window
curl -X POST http://localhost:8123/admin/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"name":"algo-service","role":"writer","symbols":["AAPL"],"tables":["trades"],"tenant_id":"desk_1","expires_at_ns":1743465600000000000}'

All fields except name are optional:

FieldTypeDefaultDescription
namestring(required)Human-readable label
rolestring"reader"admin / writer / reader / analyst / metrics
symbolsstring[][]Symbol whitelist (empty = unrestricted)
tablesstring[][]Table whitelist (empty = unrestricted)
tenant_idstring""Bind key to a tenant
expires_at_nsint640Expiry timestamp in nanoseconds (0 = never)
{"key": "zepto_a1b2c3d4e5f6..."}

The full key is shown exactly once. Store it securely.

Update mutable fields of an existing key. Only provided fields are modified.

Terminal window
curl -X PATCH http://localhost:8123/admin/keys/ak_7f3k8a2b \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"symbols":["AAPL","GOOG"],"enabled":true,"tenant_id":"desk_2","expires_at_ns":0}'
FieldTypeDescription
symbolsstring[]Replace symbol whitelist
tablesstring[]Replace table whitelist
enabledboolEnable/disable key
tenant_idstringChange tenant binding
expires_at_nsint64Change expiry (0 = never)
{"updated": true}
Terminal window
curl -X DELETE http://localhost:8123/admin/keys/ak_7f3k8a2b \
-H "Authorization: Bearer $ADMIN_KEY"
{"revoked": true}

POST /admin/auth/reload — Force refresh JWKS keys

Section titled “POST /admin/auth/reload — Force refresh JWKS keys”

Triggers an immediate re-fetch of the JWKS endpoint. Useful after IdP key rotation.

Terminal window
curl -X POST http://localhost:8123/admin/auth/reload \
-H "Authorization: Bearer $ADMIN_KEY"
{"refreshed": true, "keys_loaded": 2}

Returns 502 if no JWKS URL is configured or the fetch fails.


Full guide: SSO Integration Guide

Redirects the browser to the configured OIDC authorization endpoint. No authentication required.

GET /auth/login → 302 https://idp.example.com/authorize?response_type=code&client_id=...

Returns 503 if OIDC is not configured.

GET /auth/callback — OAuth2 code exchange

Section titled “GET /auth/callback — OAuth2 code exchange”

Handles the IdP redirect after user authentication. Exchanges the authorization code for tokens, resolves identity, creates a server-side session, and redirects to /query.

Query params: code (required), state (optional)

Returns 502 if token exchange fails, 401 if identity resolution fails.

POST /auth/session — Create session from Bearer token

Section titled “POST /auth/session — Create session from Bearer token”

Creates a server-side session from an existing Bearer token (API key or JWT). Returns a Set-Cookie header.

Terminal window
curl -X POST http://localhost:8123/auth/session \
-H "Authorization: Bearer $TOKEN"
{"session": true, "role": "writer", "subject": "user@example.com"}

Destroys the server-side session and clears the cookie. No authentication required.

Terminal window
curl -X POST http://localhost:8123/auth/logout -b "zepto_sid=SESSION_ID"
{"ok": true}

POST /auth/refresh — Refresh OAuth2 token

Section titled “POST /auth/refresh — Refresh OAuth2 token”

Refreshes the OAuth2 access token using the stored refresh token. Requires a valid session cookie.

Terminal window
curl -X POST http://localhost:8123/auth/refresh -b "zepto_sid=SESSION_ID"
{"refreshed": true, "expires_in": 3600}

Returns 401 if the session is invalid or refresh fails.

Returns the authenticated identity from session cookie or Bearer token.

Terminal window
curl http://localhost:8123/auth/me -b "zepto_sid=SESSION_ID"
# or
curl http://localhost:8123/auth/me -H "Authorization: Bearer $TOKEN"
{"subject": "user@example.com", "role": "writer", "source": "sso:okta-prod"}

GET /admin/queries — List running queries

Section titled “GET /admin/queries — List running queries”
Terminal window
curl http://localhost:8123/admin/queries -H "Authorization: Bearer $ADMIN_KEY"
[
{
"id": "q_a1b2c3",
"subject": "ak_7f3k8a2b",
"sql": "SELECT * FROM trades WHERE...",
"started_at_ns": 1711234567000000000
}
]

DELETE /admin/queries/:id — Kill a running query

Section titled “DELETE /admin/queries/:id — Kill a running query”

Cancels the query at the next partition scan boundary via CancellationToken.

Terminal window
curl -X DELETE http://localhost:8123/admin/queries/q_a1b2c3 \
-H "Authorization: Bearer $ADMIN_KEY"
{"cancelled": true}

Query parameter: ?n=<count> (default: 100).

Terminal window
curl "http://localhost:8123/admin/audit?n=50" -H "Authorization: Bearer $ADMIN_KEY"
[
{
"ts": 1711234567000000000,
"subject": "ak_7f3k8a2b",
"role": "writer",
"action": "query",
"detail": "SELECT count(*) FROM trades",
"from": "10.0.1.5"
}
]

GET /admin/sessions — Active client sessions

Section titled “GET /admin/sessions — Active client sessions”

Lists connected clients (equivalent to kdb+ .z.po).

Terminal window
curl http://localhost:8123/admin/sessions -H "Authorization: Bearer $ADMIN_KEY"
[
{
"remote_addr": "10.0.1.5",
"user": "ak_7f3k8a2b",
"connected_at_ns": 1711234567000000000,
"last_active_ns": 1711234590000000000,
"query_count": 42
}
]

Terminal window
curl http://localhost:8123/admin/nodes -H "Authorization: Bearer $ADMIN_KEY"
{
"nodes": [
{
"id": 1,
"host": "10.0.1.1",
"port": 8123,
"state": "ACTIVE",
"ticks_ingested": 5000000,
"ticks_stored": 4999800,
"queries_executed": 12345
}
]
}

Node states: ACTIVE, SUSPECT, DEAD, JOINING, LEAVING.

POST /admin/nodes — Add remote node at runtime

Section titled “POST /admin/nodes — Add remote node at runtime”
Terminal window
curl -X POST http://localhost:8123/admin/nodes \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"id":4,"host":"10.0.1.4","port":8123}'
{"added": true, "id": 4, "host": "10.0.1.4", "port": 8123}

Requires cluster mode (set_coordinator() must be called). Idempotent — adding an existing node ID is a no-op.

DELETE /admin/nodes/:id — Remove node from cluster

Section titled “DELETE /admin/nodes/:id — Remove node from cluster”
Terminal window
curl -X DELETE http://localhost:8123/admin/nodes/4 \
-H "Authorization: Bearer $ADMIN_KEY"
{"removed": true, "id": 4}

The node is removed from the routing table immediately. Safe to call for non-existent IDs.


Terminal window
curl http://localhost:8123/admin/cluster -H "Authorization: Bearer $ADMIN_KEY"
{
"mode": "standalone",
"node_count": 1,
"partitions_created": 10,
"partitions_evicted": 0,
"ticks_ingested": 5000000,
"ticks_stored": 4999800,
"ticks_dropped": 200,
"queries_executed": 12345,
"total_rows_scanned": 50000000,
"last_ingest_latency_ns": 181
}

GET /admin/metrics/history — Time-series metrics

Section titled “GET /admin/metrics/history — Time-series metrics”

Returns time-series metrics snapshots captured by the server’s internal MetricsCollector (3-second interval, 1-hour ring buffer). No external infrastructure required — ZeptoDB monitors itself.

The metrics collector is designed to never impact the main trading workload:

ProtectionDetail
Memory hard limit256 KB default (max_memory_bytes), ~3500 snapshots max
Fixed circular bufferO(1) write, zero allocation after init, no erase()
Lock-free captureAtomic write index, memory_order_relaxed reads of stats
SCHED_IDLE threadLinux: only runs when no other thread wants CPU
Response limitMax 600 snapshots per API response (30 min at 3s interval)
Client-side boundWeb UI requests ?since=<30min_ago>&limit=600
Terminal window
# All history (capped by response_limit=600)
curl http://localhost:8123/admin/metrics/history \
-H "Authorization: Bearer $ADMIN_KEY"
# Since a specific epoch-ms, with explicit limit
curl "http://localhost:8123/admin/metrics/history?since=1711234567000&limit=100" \
-H "Authorization: Bearer $ADMIN_KEY"
[
{
"timestamp_ms": 1711234567000,
"node_id": 0,
"ticks_ingested": 5000000,
"ticks_stored": 4999800,
"ticks_dropped": 200,
"queries_executed": 12345,
"total_rows_scanned": 50000000,
"partitions_created": 10,
"last_ingest_latency_ns": 181
}
]
FieldTypeDescription
timestamp_msint64Epoch milliseconds when snapshot was taken
node_iduint16Node identifier (0 for standalone)
ticks_ingesteduint64Cumulative ticks ingested at snapshot time
ticks_storeduint64Cumulative ticks stored
ticks_droppeduint64Cumulative ticks dropped
queries_executeduint64Cumulative queries executed
total_rows_scanneduint64Cumulative rows scanned
partitions_createduint64Cumulative partitions created
last_ingest_latency_nsint64Most recent ingest latency (ns)

Query parameter: ?since=<epoch_ms> — returns only snapshots with timestamp_ms >= since. Query parameter: ?limit=<N> — max snapshots to return (default: 600).


Live partition rebalancing control. Requires set_rebalance_manager() to be called on the server. Returns 503 if rebalance is not available (standalone mode without RebalanceManager).

GET /admin/rebalance/status — Current rebalance status

Section titled “GET /admin/rebalance/status — Current rebalance status”
Terminal window
curl http://localhost:8123/admin/rebalance/status -H "Authorization: Bearer $ADMIN_KEY"
{
"state": "RUNNING",
"total_moves": 10,
"completed_moves": 3,
"failed_moves": 0,
"current_symbol": "42"
}
FieldTypeDescription
statestringIDLE, RUNNING, PAUSED, or CANCELLING
total_movesintTotal partition moves planned
completed_movesintMoves successfully committed
failed_movesintMoves that failed (after retries)
current_symbolstringSymbol currently being migrated (empty if idle)

POST /admin/rebalance/start — Start rebalance

Section titled “POST /admin/rebalance/start — Start rebalance”
Terminal window
# Scale-out: add a new node
curl -X POST http://localhost:8123/admin/rebalance/start \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"action":"add_node","node_id":4}'
# Scale-in: remove a node
curl -X POST http://localhost:8123/admin/rebalance/start \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"action":"remove_node","node_id":2}'
FieldTypeDescription
actionstring"add_node", "remove_node", or "move_partitions"
node_idintTarget node ID (required for add_node/remove_node)
movesarrayArray of {symbol, from, to} objects (required for move_partitions)

Partial move example:

Terminal window
# Move specific partitions between existing nodes
curl -X POST http://localhost:8123/admin/rebalance/start \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"action":"move_partitions","moves":[{"symbol":42,"from":1,"to":2},{"symbol":99,"from":2,"to":3}]}'

Success: {"ok": true} Already running: {"ok": false, "error": "already running"}

POST /admin/rebalance/pause — Pause rebalance

Section titled “POST /admin/rebalance/pause — Pause rebalance”

Pauses after the current in-flight move completes.

Terminal window
curl -X POST http://localhost:8123/admin/rebalance/pause \
-H "Authorization: Bearer $ADMIN_KEY"
{"ok": true}

POST /admin/rebalance/resume — Resume rebalance

Section titled “POST /admin/rebalance/resume — Resume rebalance”

Resumes a paused rebalance.

Terminal window
curl -X POST http://localhost:8123/admin/rebalance/resume \
-H "Authorization: Bearer $ADMIN_KEY"
{"ok": true}

POST /admin/rebalance/cancel — Cancel rebalance

Section titled “POST /admin/rebalance/cancel — Cancel rebalance”

Cancels the current rebalance. Already-committed moves stay committed.

Terminal window
curl -X POST http://localhost:8123/admin/rebalance/cancel \
-H "Authorization: Bearer $ADMIN_KEY"
{"ok": true}

Returns an array of past rebalance events:

[
{
"action": "add_node",
"node_id": 4,
"total_moves": 3,
"completed_moves": 3,
"failed_moves": 0,
"start_time_ms": 1712930400000,
"duration_ms": 1523,
"cancelled": false
}
]
FieldTypeDescription
actionstringadd_node, remove_node, or move_partitions
node_idnumberTarget node (0 for move_partitions)
total_movesnumberTotal partition moves planned
completed_movesnumberSuccessfully completed moves
failed_movesnumberFailed moves
start_time_msnumberStart time (epoch milliseconds)
duration_msnumberTotal duration in milliseconds
cancelledbooleanWhether the rebalance was cancelled

GET /admin/version — Server version info

Section titled “GET /admin/version — Server version info”
Terminal window
curl http://localhost:8123/admin/version -H "Authorization: Bearer $ADMIN_KEY"
{"engine": "ZeptoDB", "version": "0.1.0", "build": "Mar 25 2026"}

RoleQueryIngestStatsMetricsAdmin
admin
writer
reader
analyst
metrics

API keys can be restricted to specific tables. When allowed_tables is set, queries against any other table return 403 Forbidden.

Terminal window
# Create a key restricted to trades and quotes tables only
curl -X POST https://zepto:8443/admin/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-d '{"name":"desk-1","role":"reader","tables":["trades","quotes"]}'
# This key can query trades and quotes, but not risk_positions

When allowed_tables is empty (default), the key has access to all tables. Table ACL is enforced at the HTTP layer before SQL execution — the SQL parser extracts the target table from the query and checks it against the key’s whitelist.

ACL TypeScopeExample
Symbol ACLRow-level filter by symbolallowed_symbols: ["AAPL","GOOGL"]
Table ACLTable-level access controlallowed_tables: ["trades","quotes"]

Both can be combined: a key with allowed_tables: ["trades"] and allowed_symbols: ["AAPL"] can only query AAPL data from the trades table.


When rate limiting is enabled, requests that exceed the configured threshold return:

HTTP 403 Forbidden
{"error": "Rate limit exceeded"}

Rate limits are applied per-identity (API key ID or JWT sub) and per-IP. Both checks must pass.

See Security Operations Guide — Rate Limiting for configuration details.


See also: Security Operations Guide · SSO Integration Guide · SQL Reference · Config Reference