Skip to content
DataHashi Docs

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.

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.

Terminal window
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:

Anatomy of a Semantic Query The request groups into model, metric or measure selections, dimensions, filters/segments/view, and order/limit. The engine compiles and validates the whole request against your model before running it. The response echoes the request's shape back through columns and annotation, with data in rows. REQUEST model metrics / measures named selections dimensions group by + optional grain filters · segments · view narrow the result order · limit sort & cap rows Compile & validate against your model + row-level security + cost policy RESPONSE columns dimensions → metrics → measures annotation kind + type, per column rows data — or null if empty
Every field on the request is compiled and validated against your model before it runs — the response then mirrors that shape back through columns and annotation, with the data in rows.
FieldNotes
modelOptional. Which of your models to query. Omitted = main.
metricsGoverned selections — named metrics. Available in both modes.
measuresRaw 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.
dimensionsGroup-by. Each entry is { name, grain?, entity?, relationship? }. Output columns appear in this order, before the selections.
filtersPredicates 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.
segmentsNames of reusable filter fragments defined on the model — ANDed with filters.
viewOptionally 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).
limitRow 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).

{
"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, not order_date_month).
  • annotation is order-matched to columns and describes each column’s kind and type — read it, or ignore it and use columns/rows exactly as before.
  • A result with no rows serializes rows as null, 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.

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.

HTTPCodeWhen
400bad_requestUnknown 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.
401unauthenticatedMissing or invalid bearer token.
403forbiddenA governed token referenced measures[].
404not_foundAn unknown metric/measure/dimension/model.
429over_budgetThe query would exceed your workspace’s cost policy. No Retry-After header — back off on your own schedule.
503overloadedThe engine is over capacity.

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.

Terminal window
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.

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.