Skip to content

API Endpoints

Complete reference for all OrionBelt REST API endpoints.

Health Check

GET /health

Returns the service status and version.

Response:

{
  "status": "ok",
  "version": "1.4.0"
}

Sessions

POST /v1/sessions

Create a new session. Each session has its own model store.

Request (optional):

{
  "metadata": {
    "user": "alice",
    "purpose": "revenue analysis"
  }
}

Response (201):

{
  "session_id": "a1b2c3d4e5f6",
  "created_at": "2025-01-15T10:30:00Z",
  "last_accessed_at": "2025-01-15T10:30:00Z",
  "model_count": 0,
  "metadata": {
    "user": "alice",
    "purpose": "revenue analysis"
  }
}

GET /v1/sessions

List all active sessions.

Response (200):

{
  "sessions": [
    {
      "session_id": "a1b2c3d4e5f6",
      "created_at": "2025-01-15T10:30:00Z",
      "last_accessed_at": "2025-01-15T10:35:00Z",
      "model_count": 2,
      "metadata": {}
    }
  ]
}

GET /v1/sessions/{session_id}

Get info for a specific session. Also refreshes the session's last-accessed time.

Response (200): Same as single session in list response.

Error (404): Session not found or expired.

DELETE /v1/sessions/{session_id}

Close a session and release its resources.

Response (204): No content.

Error (404): Session not found.


Session Models

POST /v1/sessions/{session_id}/models

Load an OBML semantic model into a session. The model is parsed, validated, and stored.

Single-model mode

Returns 403 Forbidden when MODEL_FILE is configured. The model is pre-loaded automatically.

Request:

{
  "model_yaml": "version: 1.0\ndataObjects:\n  Orders:\n    code: ORDERS\n    ..."
}

Response (201):

{
  "model_id": "abcd1234",
  "data_objects": 2,
  "dimensions": 3,
  "measures": 2,
  "metrics": 1,
  "warnings": []
}

Error (403): Single-model mode: model upload is disabled.

Error (422): Model has validation errors.

Error (404): Session not found.

GET /v1/sessions/{session_id}/models

List all models loaded in a session.

Response (200):

[
  {
    "model_id": "abcd1234",
    "data_objects": 2,
    "dimensions": 3,
    "measures": 2,
    "metrics": 1
  }
]

GET /v1/sessions/{session_id}/models/{model_id}

Describe a model's contents — data objects (with fields and joins), dimensions, measures, and metrics.

Response (200):

{
  "model_id": "abcd1234",
  "data_objects": [
    {
      "label": "Orders",
      "code": "WAREHOUSE.PUBLIC.ORDERS",
      "columns": ["Order ID", "Price", "Quantity"],
      "join_targets": ["Customers"]
    }
  ],
  "dimensions": [
    {
      "name": "Country",
      "result_type": "string",
      "data_object": "Customers",
      "column": "Country",
      "time_grain": null
    }
  ],
  "measures": [...],
  "metrics": [...]
}

Error (404): Model or session not found.

DELETE /v1/sessions/{session_id}/models/{model_id}

Remove a model from a session.

Single-model mode

Returns 403 Forbidden when MODEL_FILE is configured.

Response (204): No content.

Error (403): Single-model mode: model removal is disabled.

Error (404): Model or session not found.


Session Validation

POST /v1/sessions/{session_id}/validate

Validate OBML YAML within a session context. Does not store the model.

Request:

{
  "model_yaml": "version: 1.0\ndataObjects:\n  ..."
}

Response (200):

{
  "valid": true,
  "errors": [],
  "warnings": []
}

Validation failure:

{
  "valid": false,
  "errors": [
    {
      "code": "UNKNOWN_DATA_OBJECT",
      "message": "Data object 'Unknown' not found",
      "path": "dimensions.Bad.dataObject"
    }
  ],
  "warnings": []
}

Session Query Compilation & Execution

POST /v1/sessions/{session_id}/query/sql

Compile a semantic query against a model loaded in the session.

Request:

{
  "model_id": "abcd1234",
  "query": {
    "select": {
      "dimensions": ["Customer Country"],
      "measures": ["Revenue"]
    },
    "where": [
      {
        "field": "Customer Segment",
        "op": "in",
        "value": ["SMB", "MidMarket"]
      }
    ],
    "order_by": [
      { "field": "Revenue", "direction": "desc" }
    ],
    "limit": 1000
  },
  "dialect": "postgres"
}

Response (200):

{
  "sql": "SELECT ...",
  "dialect": "postgres",
  "sql_valid": true,
  "explain": {
    "planner": "Star Schema",
    "planner_reason": "All measures come from a single fact table",
    "base_object": "Orders",
    "base_object_reason": "Orders has the most joins and contains all requested measures",
    "joins": [
      {
        "from_object": "Orders",
        "to_object": "Customers",
        "join_columns": ["CUSTOMER_ID"],
        "reason": "Required for dimension 'Customer Country'"
      }
    ],
    "where_filter_count": 1,
    "having_filter_count": 0,
    "has_totals": false,
    "cfl_legs": 0
  },
  "warnings": []
}

Error responses:

Status Cause
400 Unsupported dialect
404 Model or session not found
422 Resolution error

POST /v1/sessions/{session_id}/query/execute

Compile and execute a semantic query against the configured database. Requires FLIGHT_ENABLED=true with DB_VENDOR and vendor credentials configured.

If the query has no explicit limit, a default of 10,000 rows is enforced.

Request:

{
  "model_id": "abcd1234",
  "query": {
    "select": {
      "dimensions": ["Customer Country"],
      "measures": ["Revenue"]
    },
    "limit": 100
  },
  "dialect": "postgres"
}

Response (200):

{
  "sql": "SELECT ...",
  "dialect": "postgres",
  "columns": [
    {"name": "Customer Country", "type": "string"},
    {"name": "Revenue", "type": "number"}
  ],
  "rows": [
    ["US", 15230.50],
    ["UK", 9870.00]
  ],
  "row_count": 2,
  "execution_time_ms": 42.5,
  "resolved": {
    "fact_tables": ["Orders"],
    "dimensions": ["Customer Country"],
    "measures": ["Revenue"]
  },
  "sql_valid": true,
  "warnings": [],
  "explain": { "..." : "..." }
}

Error responses:

Status Cause
400 Unsupported dialect
404 Model or session not found
422 Resolution error
502 Database execution failed
503 Query execution not available (FLIGHT_ENABLED not set)

Top-level shortcut: POST /v1/query/execute — auto-resolves session/model, auto-detects dialect from DB_VENDOR.


OSI ↔ OBML Conversion

Stateless endpoints for converting between OSI (Open Semantic Interchange) and OBML formats. No session required.

POST /v1/convert/osi-to-obml

Convert an OSI YAML model to OBML format.

Request:

{
  "input_yaml": "version: \"0.1.1\"\nsemantic_model:\n  - name: my_model\n    ..."
}

Response (200):

{
  "output_yaml": "version: 1.0\ndataObjects:\n  ...",
  "warnings": [
    "Relationship 'sales_to_date': no type specified, defaulting to many-to-one."
  ],
  "validation": {
    "schema_valid": true,
    "semantic_valid": true,
    "schema_errors": [],
    "semantic_errors": [],
    "semantic_warnings": []
  }
}

Error (400): Invalid YAML input.

Error (422): Conversion failed (e.g. unsupported OSI structure).

POST /v1/convert/obml-to-osi

Convert an OBML YAML model to OSI format.

Request:

{
  "input_yaml": "version: 1.0\ndataObjects:\n  ...",
  "model_name": "my_model",
  "model_description": "Sales analytics model",
  "ai_instructions": ""
}

The model_name, model_description, and ai_instructions fields are optional (defaults: "semantic_model", "", "").

Response (200): Same structure as POST /v1/convert/osi-to-obml.

Error (400): Invalid YAML input.

Error (422): Conversion failed.


Model Discovery

These endpoints provide structured access to model metadata. All fields include an optional owner property when set in the OBML model.

GET /v1/sessions/{session_id}/models/{model_id}/schema

Full model structure as JSON, including all data objects, dimensions, measures, and metrics.

Response (200):

{
  "model_id": "abcd1234",
  "version": 1.0,
  "owner": "team-data",
  "data_objects": [
    {
      "name": "Orders",
      "code": "ORDERS",
      "database": "WAREHOUSE",
      "schema": "PUBLIC",
      "columns": [
        { "name": "Price", "code": "PRICE", "abstract_type": "float" }
      ],
      "join_targets": ["Customers"],
      "owner": "team-sales"
    }
  ],
  "dimensions": [
    { "name": "Country", "data_object": "Customers", "column": "Country", "result_type": "string" }
  ],
  "measures": [
    { "name": "Revenue", "aggregation": "sum", "result_type": "float", "columns": [...] }
  ],
  "metrics": [
    { "name": "Revenue per Order", "expression": "...", "component_measures": ["Revenue", "Order Count"] }
  ]
}

GET /v1/sessions/{session_id}/models/{model_id}/dimensions

List all dimensions.

Response (200): Array of dimension objects.

GET /v1/sessions/{session_id}/models/{model_id}/dimensions/{name}

Get a single dimension by name.

Response (200):

{
  "name": "Country",
  "data_object": "Customers",
  "column": "Country",
  "result_type": "string",
  "time_grain": null,
  "owner": null
}

Error (404): Dimension not found.

GET /v1/sessions/{session_id}/models/{model_id}/measures

List all measures.

Response (200): Array of measure objects.

GET /v1/sessions/{session_id}/models/{model_id}/measures/{name}

Get a single measure by name.

Response (200):

{
  "name": "Revenue",
  "aggregation": "sum",
  "result_type": "float",
  "columns": [
    { "data_object": "Orders", "column": "Price" }
  ],
  "expression": "{[Orders].[Price]} * {[Orders].[Quantity]}",
  "total": false,
  "owner": null
}

Error (404): Measure not found.

GET /v1/sessions/{session_id}/models/{model_id}/metrics

List all metrics.

Response (200): Array of metric objects.

GET /v1/sessions/{session_id}/models/{model_id}/metrics/{name}

Get a single metric by name. Returns the expression formula and its component measures.

Error (404): Metric not found.

GET /v1/sessions/{session_id}/models/{model_id}/explain/{name}

Explain the lineage of a dimension, measure, or metric — traces back through the dependency chain to the underlying data objects and columns.

Response (200):

{
  "name": "Revenue",
  "type": "measure",
  "lineage": [
    { "type": "data_object", "name": "Orders" },
    { "type": "column", "name": "Price", "detail": "referenced in expression" },
    { "type": "column", "name": "Quantity", "detail": "referenced in expression" }
  ]
}

Error (404): Name not found in model.

POST /v1/sessions/{session_id}/models/{model_id}/find

Search across model artefacts by name or synonym.

Request:

{
  "query": "Revenue",
  "types": ["measure", "metric"]
}

The types filter is optional. Valid types: dimension, measure, metric, data_object.

Response (200):

{
  "results": [
    { "name": "Revenue", "type": "measure", "match": "name" },
    { "name": "Revenue per Order", "type": "metric", "match": "name" }
  ]
}

GET /v1/sessions/{session_id}/models/{model_id}/join-graph

Return the join graph as nodes and edges.

Response (200):

{
  "nodes": ["Orders", "Customers", "Products"],
  "edges": [
    {
      "from_object": "Orders",
      "to_object": "Customers",
      "cardinality": "many-to-one",
      "secondary": false
    }
  ]
}

OBSL Graph & SPARQL

GET /v1/sessions/{session_id}/models/{model_id}/graph

Return the OBSL-Core RDF graph as Turtle. The graph is generated at model load time.

Response (200): text/turtle

@prefix obsl: <https://ralforion.com/ns/obsl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://ralforion.com/ns/model/abc123> a obsl:SemanticModel ;
    obsl:hasDataObject <.../data-object/orders> ;
    obsl:hasMeasure <.../measure/revenue> .

Error (404): Session or model not found.

POST /v1/sessions/{session_id}/models/{model_id}/sparql

Execute a read-only SPARQL query against the model's OBSL graph.

Request:

{
  "query": "PREFIX obsl: <https://ralforion.com/ns/obsl#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?label WHERE { ?m a obsl:Measure ; rdfs:label ?label . }"
}

Only SELECT and ASK queries are allowed. The query field has a maximum length of 100,000 characters.

Response (200):

{
  "type": "select",
  "variables": ["label"],
  "results": [
    {"label": "Revenue"},
    {"label": "Order Count"}
  ],
  "boolean": null
}

For ASK queries:

{
  "type": "ask",
  "variables": [],
  "results": [],
  "boolean": true
}

Error (400): Update query rejected or invalid SPARQL syntax.

Error (404): Session or model not found.


Top-level Shortcuts

These endpoints auto-resolve the session and model when only one exists. They mirror the session-scoped model discovery endpoints without requiring session/model IDs.

Returns 404 if no sessions exist, 409 Conflict if multiple sessions or models exist.

Shortcut Equivalent
GET /v1/schema GET /v1/sessions/{id}/models/{mid}/schema
GET /v1/dimensions GET /v1/sessions/{id}/models/{mid}/dimensions
GET /v1/dimensions/{name} GET /v1/sessions/{id}/models/{mid}/dimensions/{name}
GET /v1/measures GET /v1/sessions/{id}/models/{mid}/measures
GET /v1/measures/{name} GET /v1/sessions/{id}/models/{mid}/measures/{name}
GET /v1/metrics GET /v1/sessions/{id}/models/{mid}/metrics
GET /v1/metrics/{name} GET /v1/sessions/{id}/models/{mid}/metrics/{name}
GET /v1/explain/{name} GET /v1/sessions/{id}/models/{mid}/explain/{name}
POST /v1/find POST /v1/sessions/{id}/models/{mid}/find
GET /v1/join-graph GET /v1/sessions/{id}/models/{mid}/join-graph
GET /v1/graph GET /v1/sessions/{id}/models/{mid}/graph
POST /v1/sparql POST /v1/sessions/{id}/models/{mid}/sparql
POST /v1/query/sql POST /v1/sessions/{id}/query/sql (auto-resolves model_id)

Settings

GET /v1/settings

Return public configuration for API clients (UI, MCP, etc.).

Response (200):

{
  "single_model_mode": false,
  "model_yaml": null,
  "session_ttl_seconds": 1800
}

When MODEL_FILE is configured:

{
  "single_model_mode": true,
  "model_yaml": "version: 1.0\ndataObjects:\n  ...",
  "session_ttl_seconds": 1800
}
Field Type Description
single_model_mode bool Whether model upload/removal is disabled
model_yaml string | null Pre-loaded OBML YAML (only when single-model mode is active)
session_ttl_seconds int Session inactivity timeout

Dialects

GET /v1/dialects

List all available SQL dialects and their capability flags.

Response (200):

{
  "dialects": [
    {
      "name": "bigquery",
      "capabilities": {
        "supports_cte": true,
        "supports_qualify": true,
        "supports_arrays": true,
        "supports_window_filters": true,
        "supports_ilike": false,
        "supports_time_travel": false,
        "supports_semi_structured": true
      }
    },
    { "name": "clickhouse", "capabilities": { "..." : true } },
    { "name": "databricks", "capabilities": { "..." : true } },
    { "name": "dremio", "capabilities": { "..." : true } },
    {
      "name": "duckdb",
      "capabilities": {
        "supports_cte": true,
        "supports_qualify": true,
        "supports_arrays": true,
        "supports_window_filters": true,
        "supports_ilike": true,
        "supports_time_travel": false,
        "supports_semi_structured": false
      }
    },
    { "name": "postgres", "capabilities": { "..." : true } },
    { "name": "snowflake", "capabilities": { "..." : true } }
  ]
}