Skip to content

Serve / Semantic query API

Semantic query API ​

A serving endpoint publishes a model release under a stable slug. Consumers ask for metrics by name, grouped and filtered by dimensions; Keel compiles the request to SQL for the source's dialect, applies access policies, runs it on the source, and returns rows with trust scores. Consumers never write SQL.

1. Publish an endpoint ​

Settings › Serving endpoints › New endpoint (needs the Govern permission), or:

bash
curl -X POST "$KEEL/api/v1/workspaces/$WS/serving/endpoints" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
        "slug": "sales",
        "settings": {
          "name": "Sales metrics",
          "modelId": "'"$MODEL"'",
          "releaseId": null,
          "allowedMetrics": [], "allowedDimensions": [],
          "allowedApiKeyIds": [],
          "exposeSql": false,
          "cacheTtlSeconds": 300,
          "rateLimitPerMinute": 60
        }
      }'
SettingMeaning
slugUnique per tenant among live endpoints; never changes.
releaseIdPin a release, or null to follow the model's latest release.
allowedMetrics / allowedDimensionsWhat the endpoint exposes; empty = everything. Anything else is 422 Serving.NotExposed with suggestions.
allowedApiKeyIdsOptional allow-list of API keys.
exposeSqlReturn compiled SQL (includeSql) and allow /compile for every caller; otherwise only for people who can edit models.
cacheTtlSecondsResult cache (default 300 s, 0 = off).
rateLimitPerMinutePer endpoint and principal (default 60); 429 with Retry-After.

Endpoints can be disabled (:disable → queries get 403 Serving.EndpointDisabled), re-enabled and deleted; every change is audited.

2. Create an API key ​

Settings › API keys › Create key, with scope serving:query (add mcp:use for MCP). The key — keel_live_… — is shown once. Keys are stored as SHA-256 hashes; revoking one takes effect everywhere within 60 s.

bash
curl -X POST "$KEEL/api/v1/workspaces/$WS/api-keys" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "name": "reporting-app", "scopes": ["serving:query"], "expiresAt": null }'

API keys are Analysts

Access policies evaluate an API key as an Analyst with no attributes: masks for Analysts apply, and row filters that need a user attribute return no rows. Use a person's OIDC token when the query must run as that person.

3. Discover what you can ask ​

bash
curl "$KEEL/api/v1/serving/sales/catalog" -H "X-Api-Key: $KEEL_API_KEY"

The catalog lists the metrics (type, certification, trust, time dimension, format), dimensions (with grains) and segments this caller may use — hidden assets are simply absent.

4. Query ​

bash
curl -X POST "$KEEL/api/v1/serving/sales/query" \
  -H "X-Api-Key: $KEEL_API_KEY" -H "Content-Type: application/json" \
  -d '{
        "metrics": ["revenue", "order_count"],
        "dimensions": ["customer.country", "order_date.month"],
        "filters": [{ "dimension": "order_date", "op": "between", "values": ["2026-01-01", "2026-06-30"] }],
        "segments": [],
        "orderBy": [{ "field": "revenue", "desc": true }],
        "limit": 100,
        "options": { "dryRun": false, "includeSql": false, "includeTrust": true }
      }'
json
{
  "columns": [{ "name": "customer.country", "kind": "…", "type": "…" }, …],
  "rows": [["Canada", "2026-01-01", 12345.5, 210], …],
  "trust": { "metrics": { "revenue": 92, "order_count": 88 }, "warnings": [] },
  "cache": { "hit": false },
  "release": { "modelName": "Sales", "version": "1.2.0", … },
  "truncated": false,
  "durationMs": 184
}
  • Dimensions are entity.dimension, entity.dimension.grain (day, week, month, quarter, year), or entity.grain when the entity has a single time dimension. Role-playing joins are addressed through the role: order_date.calendar_date.month, ship_date.month.
  • Filter operators: eq ne in not_in gt gte lt lte between is_null is_not_null contains. Every value is sent as a bound parameter.
  • Limits: 1,000 rows by default, 10,000 at most; 30 s timeout; one source per query.
  • Unknown names return 422 with name and suggestions ("did you mean") in the Problem Details.
  • POST …/compile returns the SQL and its parameters without running it (needs exposeSql or model-edit rights).

How a query is compiled ​

  1. Bind names against the endpoint's release.
  2. Plan join paths: the unique shortest path over many-to-one and one-to-one joins, honouring roles. An ambiguous path, or a dimension on the many side, is 422.
  3. Fan-out safety: each fact is aggregated at its own grain in its own CTE, then combined on the dimension values, so revenue by customer with order lines and payments returns the base fact's total.
  4. Policies: row filters at the base table, masks in the projection.
  5. SQL through a dialect AST (PostgreSQL, SQL Server, MySQL, Oracle, Redshift, Snowflake, BigQuery, Databricks); a release always compiles a given query to byte-identical SQL. p95 compile time on a 500-entity model: ~3 ms.

Caching and the query log ​

  • Results are cached in Redis per tenant, release, effective policy and normalized query — a different policy never shares an entry. Dry runs, errors and results over 5,000 rows are never cached. A Redis outage only means misses.
  • Every query is logged as metadata only — consumer, principal, metric and dimension names, SQL and policy hashes, timings, rows, cache hit, outcome; never filter values, SQL text or result rows. Retention 90 days. The log feeds lineage, impact analysis and breaking-change checks.

Rate-limit headers

Successful responses don't carry RateLimit-* headers yet; a 429 carries Retry-After.

Try it without an endpoint ​

The Playground (/playground) builds the same request with pickers, shows a chart, the result table and the compiled SQL, and lets stewards "view as" a role. It queries a model version directly (POST …/models/{id}/query:compile|run).

Reference: Serving API · error codes.

Anchor Keel is a working product name: one platform, a shared core and licensed modules (Keel Semantic, Keel Migrate). Documentation built from the repository — facts are derived from code, ADRs and the decisions log.