developerUpdated 2026-05-04

Headless API

What this covers

The headless API is a JSON-in, JSON-out query interface for non-SQL clients. It lets mobile apps, microservices, embedded analytics widgets, and custom integrations query the Tessallite semantic layer without writing SQL. Queries are expressed as lists of measure and dimension names with optional filters, and results come back as JSON arrays. This article covers authentication, the query and metadata endpoints, rate limiting, and worked examples.

When to use headless API vs JDBC/XMLA

Use caseRecommended interface
BI tools (Excel, Power BI, Tableau, DBeaver)JDBC or XMLA
Mobile appsHeadless API
Microservice-to-microservice integrationHeadless API
Embedded analytics in a web appHeadless API
Ad-hoc SQL explorationJDBC
Automated reporting scriptsEither (headless is simpler if you don't need SQL)

The headless API and JDBC/XMLA share the same semantic layer, the same aggregate routing, and the same security model. The difference is the query language: headless uses JSON measure/dimension names; JDBC/XMLA use SQL or DAX.

Authentication

The headless API uses the same JWT tokens as the Tessallite SPA. Obtain a token via the login endpoint:

TOKEN=$(curl -s -X POST http://host:3000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"tenant_id":"acme-demo","email":"admin@acme-demo.com","password":"acme-demo"}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')

Include the token in all subsequent requests:

Authorization: Bearer $TOKEN

Query endpoint

POST /query-router/api/v1/headless/query

Resolves measure and dimension names against the semantic model, builds SQL via the routing pipeline (aggregate/pocket/source), executes, and returns rows.

Request body:

{
  "project_id": "uuid",
  "model_id": "uuid",
  "measures": ["revenue", "order_count"],
  "dimensions": ["region", "order_date"],
  "filters": [
    {"dimension": "region", "operator": "eq", "value": "US"},
    {"dimension": "order_date", "operator": "between", "values": ["2025-01-01", "2025-12-31"]}
  ],
  "limit": 100,
  "offset": 0,
  "order_by": [{"field": "revenue", "direction": "desc"}]
}

Required fields: project_id, model_id, measures (at least one).

Optional fields: dimensions, filters, limit, offset, order_by.

Filter operators:

OperatorDescriptionValue field
eqEqualsvalue
neNot equalsvalue
gtGreater thanvalue
gteGreater than or equalvalue
ltLess thanvalue
lteLess than or equalvalue
inIn listvalues (array)
not_inNot in listvalues (array)
betweenBetween (inclusive)values (2-element array)
likeSQL LIKE patternvalue
is_nullIs NULL(none)
is_not_nullIs not NULL(none)

Response:

{
  "columns": ["region", "revenue", "order_count"],
  "rows": [
    {"region": "US", "revenue": 125000, "order_count": 430},
    {"region": "EU", "revenue": 98000, "order_count": 312}
  ],
  "total_rows": 2,
  "query_id": "a1b2c3d4e5f6g7h8"
}

Metadata endpoints

GET /query-router/api/v1/headless/models

Lists all models accessible to the authenticated user's tenant.

[
  {
    "id": "uuid",
    "project_id": "uuid",
    "slug": "sales-model",
    "display_name": "Sales Model",
    "description": "Revenue and order metrics"
  }
]

GET /query-router/api/v1/headless/models/{model_id}/measures

Lists all measures in the specified model.

[
  {
    "id": "uuid",
    "name": "revenue",
    "display_name": "Revenue",
    "description": "Total revenue",
    "format": "$#,##0",
    "aggregation_type": "sum",
    "variant_kind": null
  }
]

GET /query-router/api/v1/headless/models/{model_id}/dimensions

Lists all dimensions in the specified model.

[
  {
    "id": "uuid",
    "name": "region",
    "display_name": "Region",
    "description": "Sales region",
    "is_time_dim": false
  }
]

Rate limiting

The headless API enforces a per-tenant rate limit (default: 100 requests per minute). When the limit is exceeded, the API returns:

HTTP 429 Too Many Requests

Every response includes the X-RateLimit-Remaining header showing how many requests remain in the current window. Monitor this header to implement client-side throttling.

The rate limit is configurable via the HEADLESS_RATE_LIMIT environment variable.

Worked example: curl

# 1. Authenticate
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"tenant_id":"acme-demo","email":"admin@acme-demo.com","password":"acme-demo"}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')

# 2. List models
curl -s http://localhost:3000/query-router/api/v1/headless/models \
  -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

# 3. List measures for a model
MODEL_ID="<model-uuid-from-step-2>"
curl -s "http://localhost:3000/query-router/api/v1/headless/models/$MODEL_ID/measures" \
  -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

# 4. Query
curl -s -X POST http://localhost:3000/query-router/api/v1/headless/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"project_id\": \"<project-uuid>\",
    \"model_id\": \"$MODEL_ID\",
    \"measures\": [\"revenue\"],
    \"dimensions\": [\"region\"],
    \"limit\": 10
  }" | python3 -m json.tool

Pitfalls

Related