Query the API
Every query, from every consumer (REST, MCP, or the console’s explore UI), is the same Semantic Query object. It names one semantic model and some combination of metrics/measures, dimensions, filters, ordering, and a row limit — never SQL, and never a raw table or column name.
Authentication
Section titled “Authentication”Every request (except a bare status check) needs the API key you minted as a bearer token:
Authorization: Bearer <your-api-key>The token carries your workspace identity and mode (governed or explore) only. The warehouse,
row-level security, and cost limits are all resolved server-side from that identity — there is no
field on the request that can widen any of them.
Run a query
Section titled “Run a query”curl https://engine.datahashi.com/v1/query \ -H "Authorization: Bearer $DATAHASHI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "metrics": ["avg_order_value"], "dimensions": [{ "name": "region" }], "order": [{ "ref": "region" }], "limit": 100 }'Omit model to query your default model (main); name another one of your workspace’s models to
target it instead.
Here’s how the fields on that request map onto the response you get back:
columns and annotation, with the data in rows.Request fields
Section titled “Request fields”| Field | Notes |
|---|---|
model | Optional. Which of your models to query. Omitted = main. |
metrics | Governed selections — named metrics. Available in both modes. |
measures | Raw selections. Explore mode only — a non-empty list under a governed key is a 403. Each entry is a bare measure name, or { name, entity } when the name is ambiguous across entities. |
dimensions | Group-by. Each entry is { name, grain?, entity?, relationship? }. Output columns appear in this order, before the selections. |
filters | Predicates on dimensions: { dimension, op, value, entity? }. op is one of eq, ne, lt, le, gt, ge, in, not_in, contains, starts_with, ends_with, is_null. in/not_in take a non-empty array. |
segments | Names of reusable filter fragments defined on the model — ANDed with filters. |
view | Optionally scope the query to a named, curated view of the model. |
order | [{ ref, desc? }] — ref names an output column (a grouped dimension or a selection). |
limit | Row cap. If your workspace’s cost policy sets a cap, an unbounded query (or one over the cap) is refused with 429 before it runs. |
Filter values are bound parameters, never interpolated into SQL — and are validated against the matching dimension’s declared type (a date dimension expects an ISO string, not a number).
Response shape
Section titled “Response shape”{ "columns": ["region", "avg_order_value"], "annotation": [ { "name": "region", "kind": "dimension", "type": "string" }, { "name": "avg_order_value", "kind": "metric", "type": "number" } ], "rows": [ ["na", 142.50], ["emea", 138.10] ]}- Column order is fixed: every grouped dimension first (in request order), then metrics (in
request order), then measures. A grain-bucketed dimension keeps its own name as the column name
(
order_date, notorder_date_month). annotationis order-matched tocolumnsand describes each column’s kind and type — read it, or ignore it and usecolumns/rowsexactly as before.- A result with no rows serializes
rowsasnull, not[]— guard for it. - Whether the answer came from a pre-aggregated rollup or live from the warehouse is never reported — that’s the point of the abstraction. Latency is the only tell.
Errors
Section titled “Errors”Every handler-generated error is {"error": {"code", "message"}}, with one of seven stable codes:
bad_request, unauthenticated, forbidden, not_found, over_budget, overloaded,
internal. Note that “this exists but isn’t yours” and “this doesn’t exist” are both reported as
not_found — deliberately, so a query can’t be used to probe for names in another workspace.
| HTTP | Code | When |
|---|---|---|
| 400 | bad_request | Unknown grain/filter operator, a filter value that doesn’t match the dimension’s type, a grain on a non-temporal dimension, an ambiguous measure/dimension/join, or a body over 1 MiB. |
| 401 | unauthenticated | Missing or invalid bearer token. |
| 403 | forbidden | A governed token referenced measures[]. |
| 404 | not_found | An unknown metric/measure/dimension/model. |
| 429 | over_budget | The query would exceed your workspace’s cost policy. No Retry-After header — back off on your own schedule. |
| 503 | overloaded | The engine is over capacity. |
Drilling into a result cell
Section titled “Drilling into a result cell”POST /v1/drill-down is a convenience over /v1/query: given the raw measure behind an
aggregated cell and that cell’s exact dimension values, it mechanically builds and runs the
equivalent raw-measure query — same governed/explore gating, same row-level security, same cost
caps. A grain-bucketed cell value ({ "dimension": "order_date", "value": "2026-06-01", "grain": "month" }) expands to the half-open date range that bucket covers.
curl https://engine.datahashi.com/v1/drill-down \ -H "Authorization: Bearer $DATAHASHI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "measure": "revenue", "cell": [{ "dimension": "order_date", "value": "2026-06-01", "grain": "month" }], "limit": 500 }'Drill-down is explore-only, since it always selects a raw measure.
Discovering what you can ask for
Section titled “Discovering what you can ask for”Before you can query a model’s vocabulary you may need to look it up:
GET /v1/models— list your workspace’s semantic model names.GET /v1/catalog?model=— one call for a named model’s metrics, dimensions, and curated views (plus, in explore mode, its raw measures).GET /v1/describe?model=&name=— look up one metric, dimension, or measure by name.
All three take the same bearer token as /v1/query. See Use with an LLM
agent — these are also exposed as MCP tools, which is how an agent grounds
itself in your vocabulary before it ever runs a query.