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 here, but the required follower acks did not arrive within the sync window. Nothing guarantees a replica holds the write, so a failover can lose it. 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 this node is refusing writes - it no longer holds the writer lease, or its store is fenced and needs an operator. Re-read the lease to find the current holder.
Schemas
TOML manifests describe a table - its primary key, columns, indexes, and relations. The engine indexes 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.
Insert, batch, and read against a registered schema. Single-row writes are atomic; the batch endpoint accepts a JSON array (atomic in one commit) 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 durable commit). 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 engine'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 engine. SELECT, INSERT (+ RETURNING) and UPDATE execute; DELETE via /sql translates only (returns the pk, doesn't remove the row yet). BEGIN/COMMIT transactions and CREATE TABLE are supported too. Aggregates, OUTER JOINs, and chained 3+ table joins (up to 32) 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. The IVF-PQ index compresses each vector for large corpora; no measured result is published at 100M scale. 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.
Point the official @elastic client - or any Elasticsearch 7.x REST client - at /v1/tenants/:tenant/es/. The cluster reports version 7.14.2. Documents commit with the row, so a write is searchable immediately, and row-level security and column masking apply to the query itself. Search also answers the filters aggregation and the term suggester, and a _bulk action may carry if_seq_no so a write lands only if nobody changed the document first. New here? Start with Connect an Elasticsearch client.
PUT/v1/tenants/:tenant/es/:index- Create an index and its mapping. Field types are the Elasticsearch types your client already sends.
PUT/v1/tenants/:tenant/es/:index/_doc/:id- Index (create or replace) a document by _id. Searchable the instant the call returns - no refresh to wait on.
POST/v1/tenants/:tenant/es/_bulk- Bulk index / update / delete. NDJSON: an action line, then (for index/update) a source line. The fast path for ingest.
POST/v1/tenants/:tenant/es/:index/_search- Search with the Query DSL, with aggregations in the same request. RLS and column masking apply to the query itself.
GET/v1/tenants/:tenant/es/:index/_doc/:id- Read one document by id. HEAD on the same path answers existence only, and /_source/:id returns the document with no envelope.
request
curl "$OC_BASE_URL/v1/tenants/$OC_TENANT/es/shop.products/_doc/sku-8842" \
-H "Authorization: Bearer $OC_API_KEY"
# bare document, one field
curl "$OC_BASE_URL/v1/tenants/$OC_TENANT/es/shop.products/_source/sku-8842?_source_includes=price" \
-H "Authorization: Bearer $OC_API_KEY"
POST/v1/tenants/:tenant/es/:index/_delete_by_query- Delete matching documents. Honors max_docs; an unbounded whole-table delete is refused rather than run.
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.
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 the log from `since_segment` onwards as hex-encoded entries. Management-plane convenience; production followers stream raw bytes via the dedicated transport.
All three are public (no auth) so load balancers and Prometheus scrapers can probe without a credential.
GET/health- Liveness - process is up and the log is mounted.
request
curl "$OC_BASE_URL/health"
response
200 OK
{ "status": "ok" }
GET/ready- Readiness - the bare call reports storage health and answers 200 even when `status` is degraded. Use `?require=write` (or `?require=read`) for the strict probe; `pressure` carries the reason. Follower lag is not part of the check.
# HELP oc_query_latency_ms /v1/query end-to-end latency.# TYPE oc_query_latency_ms histogramoc_query_latency_ms_bucket{le="10"} 9218oc_replication_frames_total 41702oc_plan_cache_hits_total 41190...
SQL - scope today
NATURAL JOIN and
? bind-param syntax are not in scope today.
UNION/INTERSECT/EXCEPT,
CTEs (WITH / WITH RECURSIVE),
ALTER TABLE and $1 positional binding all ship.
Correlated subqueries do ship: WHERE EXISTS / NOT EXISTS,
col IN (SELECT ...) and correlated scalar comparisons all run.
(SELECT/INSERT/UPDATE,
CREATE TABLE, and BEGIN/COMMIT execute; DELETE via /sql translates only.)
Use explicit JOIN ... ON for multi-table reads.