Skip to content

http api ​

Everything the web UI does goes through this API. The probes and the metrics are at the root (/healthz, /readyz, /metrics, /mcp). Every other endpoint is under /v1.

A failure is {"error": "…"}, with a status that says what kind of failure it is. The access level of each endpoint is in its heading. See access for the tokens.

A server describes itself at /v1/openapi.json, with one typed path for the rows of each table. This site has the document of the retail example at /openapi.json.

tables ​

The data: writing rows into tables, and looking at what they hold. Tables are declared in tables/*.yaml; every table gets these endpoints.

POST /v1/tables/{table}/rows ​

Write rows into a table · access: ingest

The body is the rows, in one of three formats, chosen by Content-Type: application/json (an array of row objects), application/x-ndjson (one row object per line) or application/vnd.apache.arrow.stream (an Arrow IPC stream, matched to columns by field name).

Values are checked against the table's column types and never coerced. A batch is all or nothing: one bad value rejects it with every problem listed by row and column, and nothing is written. On an upsert table a row whose key exists replaces the old row, and a row equal to the one stored is skipped: it counts as neither written nor replaced, and a batch of such rows changes nothing and is visible at once.

Rows become visible to queries at the next epoch (within epoch_interval); ?wait=visible answers only once they are. An Idempotency-Key header makes retries safe: a key seen in the last 24 hours gets the first response again and writes nothing. A 503 means the server is busy; retry after Retry-After seconds.

path parameterdescriptionexample
tableThe table, by name.orders
query parameterdescriptionexample
waitvisible: answer once the rows are visible to queries.visible

Request: rows: JSON, NDJSON or Arrow IPC. The rows.

statusbodydescription
200IngestResponseWritten.
400IngestErrorThe body didn't decode, or a value doesn't fit its column. Nothing was written.
404ApiErrorNo such table.
409ApiErrorA request with the same Idempotency-Key is still being processed, or the table was removed by a config change meanwhile.
415ApiErrorA Content-Type that isn't one of the three formats.
503ApiErrorBusy: the write queue is full. Retry after Retry-After seconds.

DELETE /v1/tables/{table}/rows ​

Delete rows from an upsert table, by key · access: ingest

The body lists keys, in the same formats as writing: each row holds the table's primary key columns and nothing else. Keys that match no row are counted in missing, not treated as errors. Only upsert tables have keys, so only they can be deleted from this way.

path parameterdescriptionexample
tableThe table, by name.customers
query parameterdescriptionexample
waitvisible: answer once the deletes are visible to queries.visible

Request: rows: JSON, NDJSON or Arrow IPC. The keys of the rows to delete.

statusbodydescription
200IngestResponseDeleted.
400IngestErrorThe body didn't decode, or the table isn't an upsert table.
404ApiErrorNo such table.
409ApiErrorThe table was removed by a config change meanwhile.
503ApiErrorBusy: retry after Retry-After seconds.

PUT /v1/tables/{table}/rows ​

Replace an upsert table's contents · access: ingest

The body is the table's complete new contents, in the same formats as writing: its rows are written (a row whose key exists replaces the old one), and every row whose key isn't among them is deleted, in one batch that becomes visible at once. For keeping a dimension in step with a system of record that can only be read whole (an asset tree, a product catalogue): send all of it every time, and what disappeared there disappears here. Rows that are already as sent are skipped, so sending the same contents again changes nothing. replaced counts rows replaced and rows deleted. Only upsert tables have keys, so only they can be replaced.

path parameterdescriptionexample
tableThe table, by name.customers
query parameterdescriptionexample
waitvisible: answer once the new contents are visible to queries.visible

Request: rows: JSON, NDJSON or Arrow IPC. Every row the table should hold.

statusbodydescription
200IngestResponseReplaced.
400IngestErrorThe body didn't decode, a value doesn't fit its column, or the table isn't an upsert table. Nothing was written.
404ApiErrorNo such table.
409ApiErrorA request with the same Idempotency-Key is still being processed, or the table was removed by a config change meanwhile.
415ApiErrorA Content-Type that isn't one of the three formats.
503ApiErrorBusy: the write queue is full. Retry after Retry-After seconds.

GET /v1/tables ​

Every table, with its columns and how much it holds · access: read

The tables declared in tables/*.yaml, with live row counts, write rates and memory, as of the latest epoch.

statusbodydescription
200list of TableInfoThe tables, in name order.

GET /v1/tables/{table} ​

One table · access: read

The same as one entry of GET /v1/tables.

path parameterdescriptionexample
tableThe table, by name.orders
statusbodydescription
200TableInfoThe table.
404ApiErrorNo such table.

GET /v1/tables/{table}/preview ​

The newest rows of a table · access: read

Newest first, decoded: decimals, dates and timestamps as strings, so nothing is lost on the way through JSON.

path parameterdescriptionexample
tableThe table, by name.orders
query parameterdescriptionexample
limitHow many rows, at most 1000. Default 100.10
statusbodydescription
200PreviewThe rows.
404ApiErrorNo such table.

queries ​

Asking the model: metrics grouped by fields, under a selection. Names are the semantic layer's (model.ossie.yaml), not tables'.

POST /v1/query ​

Run a query · access: read

Metrics grouped by dimensions, under a selection, from the newest snapshot: the engine dashboards use, without a subscription. Filters are one-directional: a metric is filtered by the fields its table reaches through relationships, and the rest don't apply to it.

query parameterdescriptionexample
formatcsv for the answer as CSV (a header of column names, then the rows), for spreadsheets and exports. JSON otherwise.json

Request: QueryRequest. What to compute.

statusbodydescription
200QueryResult or CSVThe answer.
400ApiErrorThe query names something the model doesn't have, or asks for something the engine can't do.

POST /v1/rows ​

Read the newest rows · access: read

The newest rows of a table that pass the filters, newest first (the order they arrived in): a log tail. A contains filter searches a string field for a text, ignoring the case of ASCII letters, and is fast on encoding: plain columns of the table itself. Filters are one-directional, as in a query: the fields the table reaches through relationships filter its rows.

query parameterdescriptionexample
formatcsv for the rows as CSV, JSON otherwise.json

Request: RowsRequest. Which rows, and which of their fields.

statusbodydescription
200QueryResult or CSVThe rows, newest first, one column per field.
400ApiErrorA field the model doesn't have, or no table reaching all of them.

GET /v1/ws ​

Live dashboards (WebSocket) · access: read

Upgrades to a WebSocket speaking the live protocol: JSON messages tagged by type (ClientMessage in, ServerMessage out). A session holds a selection with back/forward history and shows one dashboard; the server sends one frame per epoch with whatever changed on it, and answers selection changes right away. Browsers can't set headers on a WebSocket, so the read token may also come as ?token=.

query parameterdescriptionexample
tokenThe read token, when one is configured.s3cret
statusbodydescription
101noneSwitching to the WebSocket protocol.

POST /v1/views ​

Make a view · access: read

A view is a dashboard that lives only in the server's memory: checked by the same rules as a dashboard file and shown live like one (open_dashboard with its id), but never written to the config, versioned or committed. For an answer an agent shows once; saving it as a dashboard is a config change like any other. The body is a dashboard file as JSON (the dashboard schema); a visual without at is placed for you. A view nobody opens for 24 hours is forgotten, and at most 1000 are kept.

Request: JSON. A dashboard file, as JSON.

statusbodydescription
200ViewInfoThe view: its id, and what it shows.
422ViewProblemsThe dashboard doesn't compile: its problems.

GET /v1/views/{id} ​

A view · access: read

A view made with POST /v1/views: what it shows, and its dashboard file as YAML, ready to save under dashboards/.

path parameterdescriptionexample
idThe view's id, as making it answered.~3f9c2a7d41b0e85c6d2f1a9b7e4c3d20
statusbodydescription
200ViewInfoThe view.
404ApiErrorNo such view, or it was forgotten.

GET /v1/sessions/{id}/selection ​

A live session's selection · access: read

What a person looking at a dashboard or view has selected, by the session id welcome gave their connection (an MCP App tells the host's model the id with every selection): an agent reads what the person is looking at.

path parameterdescriptionexample
idThe session's id.s3f9c2a7d41b0e85c6d2f1a9b7e4c3d20
statusbodydescription
200SessionSelectionThe selection.
404ApiErrorNo such session.

PUT /v1/sessions/{id}/selection ​

Set a live session's selection · access: read

Replaces a session's selection, whole, with these conditions (written as a query's filters): an agent points the person at something. It is one step in the session's history, so Back undoes it, and the session's dashboard is refreshed at once.

path parameterdescriptionexample
idThe session's id.s3f9c2a7d41b0e85c6d2f1a9b7e4c3d20

Request: SelectionChange. The new selection.

statusbodydescription
200SessionSelectionThe selection now.
400ApiErrorA field or value that isn't the model's.
404ApiErrorNo such session.

GET /v1/model ​

The semantic model · access: read

Datasets and their fields, relationships, and metrics with their SQL, anchor table and the dashboards that show them: what the model browser shows.

statusbodydescription
200ModelInfoThe model.

POST /mcp ​

MCP for agents · access: read

The Model Context Protocol over Streamable HTTP: JSON-RPC 2.0 messages, each answered with one JSON body. tools/list lists the tools (the MCP reference has each one) and resources/list the config files' JSON Schemas. Each tool is the HTTP endpoint it wraps, called with the caller's credentials, so tools that change the config need the admin token and ingest_rows the ingest token. apply_change applies what agents.auto_apply allows and proposes the rest.

Request: JSON. A JSON-RPC message, or a batch of them.

statusbodydescription
200JSONThe JSON-RPC reply (or replies).
202noneA notification: nothing to reply.

config ​

Changing what the server runs while it runs: the config files, plans of what a change does, applying it, the versions so far and changes waiting for approval. Every change goes through the same pipeline, and is written back to the config directory.

GET /v1/config ​

The config · access: admin

Every config file of the current version, the version the server runs (behind while a change is rebuilt), and edits in the config directory that aren't applied.

statusbodydescription
200ConfigStateThe config.

POST /v1/config/plan ​

What a change would do · access: admin

Compiles the config with the change and compares it with the current one: every problem, located, and every change with its class (safe, rebuild, destructive). Changes nothing.

Request: ConfigChange. Files to set or remove, against a version.

statusbodydescription
200PlanInfoThe plan (also when the config has errors: valid says).
409ConfigErrorbase_version isn't the current version.

POST /v1/config/apply ​

Apply a change · access: admin

Plans the change and applies it: safe changes are live at the next epoch, rebuilds when their background rebuild is done (the old version serves until then). A destructive change needs approve. With propose it waits in the approval queue instead. Applied changes are written back to the config directory, and committed when it is a git repository.

Request: ConfigChange. Files to set or remove, against a version.

statusbodydescription
200ApplyResultApplied, rebuilding, queued, or nothing to change.
409ConfigErrorbase_version isn't current, the change destroys data and isn't approved, or a rebuild is still running.
422ConfigErrorThe config doesn't compile with the change.

GET /v1/config/versions ​

Versions so far · access: admin

Every applied version, newest first: who, when, from where, and what changed. Reverting is applying an old version's files.

statusbodydescription
200list of VersionInfoThe versions.

GET /v1/config/versions/{version} ​

One version's files · access: admin

The files of an applied version, to compare or revert to.

path parameterdescriptionexample
versionThe version number.1
statusbodydescription
200ConfigVersionThe version.
404ApiErrorNo such version.

GET /v1/changes ​

Changes waiting for approval · access: admin

Proposed changes, and edits in the config directory that aren't safe to apply on their own, with their plans.

statusbodydescription
200list of PendingChangeThe pending changes.

POST /v1/changes/{id}/approve ​

Approve a pending change · access: admin

Applies it, destructive or not. It must still be against the current version.

path parameterdescriptionexample
idThe pending change.1
statusbodydescription
200ApplyResultApplied or rebuilding.
404ApiErrorNo such pending change.
409ConfigErrorIt was made against an older version, or a rebuild is running.

POST /v1/changes/{id}/reject ​

Reject a pending change · access: admin

Drops it from the queue.

path parameterdescriptionexample
idThe pending change.1
statusbodydescription
200PendingChangeThe change, now dropped.
404ApiErrorNo such pending change.

server ​

The process itself: probes for orchestrators, and what server this is.

GET /v1/status ​

What the server is doing · access: read

Epoch, memory per table and for caches, live sessions, and the write-ahead log and checkpoints when data is kept on disk.

statusbodydescription
200StatusInfoThe status.

POST /v1/auth/embed ​

A token for embedded views · access: read

A short-lived token that opens only the views named, over /v1/ws (as ?token=) and nowhere else: what a view shown inside another app (an MCP App in Claude or ChatGPT) connects with, so the read token never leaves the server. It ends after an hour unused, or when the server stops.

Request: EmbedRequest. The views it may open.

statusbodydescription
200EmbedTokenThe token.
404ApiErrorOne of the views isn't there.

POST /v1/auth/session ​

Start a browser session · access: public

Trades the token in Authorization: Bearer <token> for a session cookie (HttpOnly, SameSite=Strict), good for the levels the token is: the UI does this so tokens stay out of URLs and logs. Sessions end after 12 idle hours, on sign-out, or when the server stops.

statusbodydescription
200SessionInfoThe session's levels; the cookie is set.
400ApiErrorNo bearer token.
401ApiErrorThe token is good for nothing here.

GET /v1/auth/session ​

What this request may do · access: public

The levels open to this request: by its session cookie, its bearer token, or because they need no token.

statusbodydescription
200SessionInfoThe levels.

DELETE /v1/auth/session ​

Sign out · access: public

Ends this browser's session and clears its cookie.

statusbodydescription
200SessionInfoSigned out: what's left open to everyone.

GET /metrics ​

Prometheus metrics · access: read

The server in the Prometheus text format: rows, memory and segments per table, ingest requests and rows, epoch publish and live refresh times, query times, sessions, the log and checkpoints, a running rebuild. At the root, where scrapers look.

statusbodydescription
200textThe metrics.

GET /healthz ​

Liveness probe · access: public

Answers 200 whenever the process is serving HTTP at all. It says nothing about whether the data is ready to query; that is GET /readyz.

statusbodydescription
200ProbeStatusThe process is up.

GET /readyz ​

Readiness probe · access: public

Answers 200 once the server can take traffic, and 503 while it is still starting. Later, starting includes applying the config and recovering data from the WAL, which is why this is separate from GET /healthz: a restart should not be killed for being slow to recover.

statusbodydescription
200ProbeStatusReady.
503ProbeStatusUp, but not ready yet.

GET /v1/info ​

Which server this is · access: public

The server's name, version and API version.

statusbodydescription
200ServerInfoThe server's identity.

reference ​

The API describing itself.

GET /v1/openapi.json ​

This API, as OpenAPI 3.1 · access: public

Generated from the same endpoint table the router is built from, so it can't describe a route that isn't served.

statusbodydescription
200JSONThe OpenAPI document.

GET /v1/schemas/{kind} ​

The JSON Schema of a config file · access: public

For editors (yaml-language-server), the config editor and agents writing config. Kinds: server (tidepool.yaml), table (tables/.yaml), model (model.ossie.yaml), dashboard (dashboards/.yaml), and the JSON inside TIDEPOOL extensions: metric-extension, field-extension, dataset-extension, relationship-extension, model-extension.

path parameterdescriptionexample
kindWhich kind of config file.table
statusbodydescription
200JSONThe schema.
404ApiErrorNo such kind.

reference generated from the schemas, the endpoint table and the MCP tools: just docs