One base URL per tenant instance. TLS 1.3 only. Bearer auth on every
/v1/... path; mutating routes additionally honour
Idempotency-Key. Request bodies cap at 8 MiB
(the NDJSON batch route lifts that cap and applies its own per-line + total-buffer accounting).
auth & headers
Header
Required
Notes
Authorization: Bearer <token>
Every /v1/* route
Tenant-scoped. /health, /ready, /metrics are public.
Idempotency-Key: <ulid|uuid>
Optional, mutating routes
Same key + same body = cached response. Different body with same key = 409.
Content-Type
POST / PUT
application/json by default; text/plain for schemas; application/x-ndjson on the streaming batch route.
Accept: text/event-stream
/v1/tenants/:t/watch
SSE stream. Server pushes one event per change burst.
X-OC-Query-Id
Response only
ULID for the query. Pass it to POST /v1/queries/:id/cancel.
X-OC-Replication: degraded
Response only
Write succeeded but follower didn't ack within sync-timeout. Surface as a warning.
Retry-After: <seconds>
Response only (429)
Honour it. Clients should back off, not hammer.
errors
Every non-2xx response is a JSON document of the form
{ "error": "code", "message": "...", "request_id": "..." }.
Quote request_id in support tickets.
Status
Code
Meaning
400
validation_failed
Body or query parameters malformed.
401
unauthorized
Bearer missing, invalid, or not scoped to this tenant.
402
quota_exceeded
Authed and under RPS, but credit is exhausted.
403
forbidden
Token cannot reach this resource.
404
not_found
Schema / row / migration not registered.
409
conflict
Idempotency replay-mismatch, lease busy, or migration wrong-state.
5xx; retry with backoff if idempotent. 503 means the writer was fenced — retry against the new leader.
Schemas
TOML manifests describe a table — its primary key, columns, indexes, and relations. The substrate hashes everything off the manifest, so registering one is the prerequisite to any row write.
POST/v1/tenants/:tenant/schemas— Register or update a TOML manifest. Body is the raw TOML; Content-Type: text/plain.
id = "trading.orders"
[primary_key]
columns = ["order_id"]
[[columns]]
name = "order_id"
type = "str"
[[columns]]
name = "symbol"
type = "str"
indexed = true
[[columns]]
name = "qty"
type = "i64"
response
200 OK
{ "id": "trading.orders", "version": 1 }
GET/v1/tenants/:tenant/schemas— List every schema id registered for the tenant.
# text/plain
id = "trading.orders"
[primary_key]
columns = ["order_id"]
...
Rows (typed CRUD)
Insert, batch, and read against a registered schema. Single-row writes are atomic; the batch endpoint accepts a JSON array (atomic in one WAL frame) or NDJSON via `application/x-ndjson` (streamed in flushable chunks).
POST/v1/tenants/:tenant/rows/:schema— Upsert a single row. `?expect=insert` skips the prior-state read for pure-insert bulk loads. Send `Idempotency-Key` to make retries safe.
POST/v1/tenants/:tenant/rows/:schema/_batch— Atomic batch (one WAL frame, one fsync). JSON body: a row array. NDJSON body (Content-Type: application/x-ndjson): streamed; flushes every `?chunk=N` rows (default 1000, max 10000). 8 MiB body cap is disabled on this route.
The substrate's native execution surface. POST a JSON Plan tree and get rows back. `?explain=true` returns the executed plan annotated with stats (EXPLAIN ANALYZE). Cancel an in-flight plan with the ULID handed back in `X-OC-Query-Id`.
POST/v1/tenants/:tenant/query— Execute a Plan tree. Bare response is `Vec<row>`; with `?explain=true` it's `{rows, explain}`.
POST/v1/queries/:id/cancel— Flip the cancellation token for an in-flight plan. The id is the ULID returned in `X-OC-Query-Id` on the original request.
request
curl -X POST "$OC_BASE_URL/v1/queries/01HW7G5...JZ/cancel" \
-H "Authorization: Bearer $OC_TOKEN"
response
200 OK (always; cancelled=false if already finished)
{ "cancelled": true }
GET/v1/tenants/:tenant/watch— Server-Sent Events stream. The connection holds open; the server pushes one event per change burst against the subscribed schemas. Ctrl-C / client close ends the subscription.
A SQL surface over the same substrate. SELECT executes; INSERT and DELETE return the translated payload so callers can replay them against the typed /rows path (which has the idempotency-key plumbing). Aggregates, OUTER JOINs, and chained 3+ table joins are supported.
curl -X POST "$OC_BASE_URL/v1/tenants/$OC_TENANT/sql" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "sql": "SELECT order_id, symbol, qty FROM trading.orders WHERE status = '"'"'pending'"'"' LIMIT 10" }'
{
"sql": "SELECT symbol, SUM(qty) AS shares FROM trading.orders GROUP BY symbol HAVING SUM(qty) > 1000"
}
{
"sql": "SELECT u.email, o.order_id FROM trading.orders o INNER JOIN trading.users u ON o.user_id = u.user_id WHERE o.status = 'pending'"
}
{
"sql": "SELECT u.email, t.exchange, o.symbol FROM trading.orders o INNER JOIN trading.users u ON o.user_id = u.user_id INNER JOIN trading.trades t ON o.order_id = t.order_id"
}
{
"sql": "SELECT u.email, o.order_id FROM trading.users u LEFT OUTER JOIN trading.orders o ON o.user_id = u.user_id"
}
response
200 OK · 400 parse / unsupported
// SELECT
{ "kind": "select", "rows": [{"order_id":"o-1","symbol":"AAPL","qty":100}, ...] }
// INSERT (re-issue against /rows/:schema with the returned rows)
{ "kind": "insert", "schema": "trading.orders", "rows": [...] }
// DELETE (re-issue against /rows/:schema/:pk)
{ "kind": "delete", "schema": "trading.orders", "pk": "o-1" }
Vector search
HNSW ANN with cosine / dot / L2 metrics and tunable speed/recall. Default high_recall mode hits recall@10 = 0.96 at 100k vectors with p99 109 ms; fast mode runs p99 37 ms at recall 0.69. Optional metadata is stored alongside each vector and queryable as an equality filter on topk. Brute-force fallback for small N.
POST/v1/tenants/:tenant/vector/:table/put— Upsert one vector. Optional `metadata` object is indexed for filtered topk.
GET/v1/tenants/:tenant/graph/:schema/reverse?rel=&pk=— Inbound one-hop: who points AT `pk` along `rel`. Works only when `from_table != to_table` (see oc-graph STATUS for the self-relation caveat).
GET/v1/tenants/:tenant/graph/:schema/path?rel=&src=&dst=&max_depth=— Reachability check: is there an `rel`-path from `src` to `dst` within `max_depth` hops?
GET/v1/tenants/:tenant/graph/:schema/dijkstra?rel=&src=&dst=&weights_json=— Weighted shortest-path. `weights_json` is a JSON object mapping `"<from>|<to>"` -> f64. A manifest weight-column variant is available — contact support.
Submit a diff, watch backfill progress, then cut over atomically. Aborts are allowed pre-cutover only. Spec 06 §4.4 — every state transition is durably journaled.
POST/v1/tenants/:tenant/migrations— Submit a migration. Body: `{schema, diff}` where `diff` is a `Vec<oc_migrate::DiffOp>`.
POST/v1/tenants/:tenant/migrations/:id/cutover— Atomic cutover. Only legal in `ReadyToCutover` state.
request
curl -X POST "$OC_BASE_URL/v1/tenants/$OC_TENANT/migrations/0192ab.../cutover" \
-H "Authorization: Bearer $OC_TOKEN"
response
200 OK · 409 wrong state
{ "id": "...", "state": "Completed", ... }
POST/v1/tenants/:tenant/migrations/:id/abort— Abort. Only legal pre-cutover. Once the migration is `Completed`, abort returns 409.
request
curl -X POST "$OC_BASE_URL/v1/tenants/$OC_TENANT/migrations/0192ab.../abort" \
-H "Authorization: Bearer $OC_TOKEN"
response
200 OK · 409 already cut over
{ "id": "...", "state": "Aborted", ... }
GET/v1/tenants/:tenant/migrations/_audit— Append-only audit log of every state transition (submit / cutover / abort / auto-cutover) with actor + UNIX timestamp.
Lease-driven active-passive coordination. The lease holder is the sole writer; followers tail frames from the leader. These endpoints are operational, not application-facing — most tenants never call them.
GET/v1/replication/lease— Read the current lease (or null if vacant).
GET/v1/replication/frames?since_segment=&epoch=— Export every WAL frame from `since_segment` onwards as hex-encoded `Frame::Append`. Management-plane convenience; production followers stream raw bytes via the FrameReader transport.
# HELP oc_query_latency_ms /v1/query end-to-end latency.
# TYPE oc_query_latency_ms histogram
oc_query_latency_ms_bucket{le="10"} 9218
oc_replication_frames_total 41702
oc_plan_cache_hits_total 41190
...
SQL — what's not supported
UPDATE,
BEGIN/COMMIT/multi-statement transactions,
DDL (CREATE TABLE / ALTER),
CROSS JOIN, and
NATURAL JOIN are not in scope today.
Use POST /rows/:schema for upserts, the
migrations
endpoints for schema change, and explicit JOIN ... ON for multi-table reads.