Dashboards API
REST API endpoints for dashboard management.
Base URL: /api/dashboards
Authentication
All endpoints except public dashboard access require authentication via Bearer token:
Authorization: Bearer <token>
Dashboard Operations
Create Dashboard
POST /api/dashboards/
Request Body:
{
"name": "Sales Dashboard",
"description": "Monthly sales overview",
"workspace_id": "uuid-optional",
"layout": {
"columns": 12,
"rowHeight": 80,
"gap": 16
},
"refresh_interval_seconds": 300
}
Response (201 Created):
{
"id": "uuid",
"name": "Sales Dashboard",
"description": "Monthly sales overview",
"user_id": "user-id",
"workspace_id": null,
"layout": { "columns": 12, "rowHeight": 80, "gap": 16 },
"refresh_interval_seconds": 300,
"is_public": false,
"share_token": null,
"is_archived": false,
"created_at": "2024-01-15T10:00:00Z",
"tile_count": 0,
"owner_name": "John Doe"
}
List Dashboards
GET /api/dashboards/
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| workspace_id | UUID | null | Filter by workspace |
| include_archived | boolean | false | Include archived dashboards |
| search | string | null | Search in name/description |
| limit | integer | 50 | Max results (1-100) |
| offset | integer | 0 | Pagination offset |
Response (200 OK):
[
{
"id": "uuid",
"name": "Sales Dashboard",
"description": "Monthly sales overview",
"tile_count": 6,
"is_public": false,
"owner_name": "John Doe",
"workspace_name": null,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T12:00:00Z"
}
]
Get Dashboard with Tiles
GET /api/dashboards/{dashboard_id}
Response (200 OK):
{
"id": "uuid",
"name": "Sales Dashboard",
"tiles": [
{
"id": "tile-uuid",
"dashboard_id": "uuid",
"title": "Revenue by Region",
"tile_type": "chart",
"chart_type": "bar",
"datasource_id": "ds-uuid",
"catalog": "analytics",
"schema": "sales",
"query": "SELECT region, SUM(revenue) FROM sales GROUP BY region",
"chart_config": { "xField": "region", "yField": "revenue" },
"grid_x": 0,
"grid_y": 0,
"grid_w": 6,
"grid_h": 4,
"created_at": "2024-01-15T10:00:00Z"
}
],
"tile_count": 1,
"owner_name": "John Doe"
}
Update Dashboard
PUT /api/dashboards/{dashboard_id}
Request Body (all fields optional):
{
"name": "Updated Name",
"description": "Updated description",
"layout": { "columns": 12, "rowHeight": 100, "gap": 20 },
"refresh_interval_seconds": 600,
"is_archived": false
}
Delete Dashboard
DELETE /api/dashboards/{dashboard_id}
Response: 204 No Content
Archive Dashboard
POST /api/dashboards/{dashboard_id}/archive
Response: 204 No Content
Sharing
Share/Unshare Dashboard
POST /api/dashboards/{dashboard_id}/share
Request Body:
{
"is_public": true
}
Response (200 OK):
{
"is_public": true,
"share_token": "abc123...",
"share_url": "/dashboards/shared/abc123..."
}
Get Public Dashboard
GET /api/dashboards/shared/{share_token}
No authentication required.
Returns the dashboard structure and tiles (same format as Get Dashboard).
Get Public Dashboard Data
GET /api/dashboards/shared/{share_token}/data
No authentication required.
Returns the latest snapshot data (cached query results, not live queries).
Response (200 OK):
{
"id": "snapshot-uuid",
"dashboard_id": "uuid",
"tile_data": {
"tile-uuid": {
"data": {
"columns": ["region", "revenue"],
"rows": [
{ "region": "North", "revenue": 150000 },
{ "region": "South", "revenue": 120000 }
]
},
"error": null,
"executed_at": "2024-01-15T12:00:00Z"
}
},
"execution_time_ms": 1234,
"trigger_type": "on_share",
"created_at": "2024-01-15T12:00:00Z"
}
Tile Management
Add Tile
POST /api/dashboards/{dashboard_id}/tiles
Request Body:
{
"title": "Revenue Chart",
"tile_type": "chart",
"chart_type": "bar",
"datasource_id": "uuid",
"catalog": "analytics",
"schema": "sales",
"query": "SELECT region, revenue FROM sales",
"chart_config": {
"xField": "region",
"yField": "revenue",
"showLegend": true
},
"grid_x": 0,
"grid_y": 0,
"grid_w": 6,
"grid_h": 4
}
Tile Types: chart, kpi, table, text
Chart Types: bar, line, area, pie, scatter, funnel, heatmap, treemap, sunburst, sankey, chord, radar, gauge, boxplot, candlestick
Add Tile from Conversation
POST /api/dashboards/{dashboard_id}/tiles/from-conversation
Add a chart generated in an AI conversation to a dashboard.
Request Body:
{
"title": "Chart from AI",
"conversation_id": "conv-id",
"message_id": "msg-id",
"chart_type": "bar",
"query": "SELECT * FROM sales",
"chart_config": { "xField": "region", "yField": "revenue" },
"datasource_id": "uuid",
"catalog": "analytics",
"schema": "sales",
"grid_w": 4,
"grid_h": 3
}
Update Tile
PUT /api/dashboards/{dashboard_id}/tiles/{tile_id}
Request Body (all fields optional):
{
"title": "Updated Title",
"query": "SELECT * FROM new_table",
"chart_config": { "xField": "col1", "yField": "col2" },
"grid_x": 4,
"grid_y": 0,
"grid_w": 8,
"grid_h": 4
}
Delete Tile
DELETE /api/dashboards/{dashboard_id}/tiles/{tile_id}
Response: 204 No Content
Bulk Update Tile Layouts
PUT /api/dashboards/{dashboard_id}/tiles/layout
Update positions of multiple tiles at once (typically after drag-drop operations).
Request Body:
{
"layouts": [
{ "tile_id": "uuid-1", "grid_x": 0, "grid_y": 0, "grid_w": 6, "grid_h": 4 },
{ "tile_id": "uuid-2", "grid_x": 6, "grid_y": 0, "grid_w": 6, "grid_h": 4 }
]
}
Response: 204 No Content
Query Execution
Execute All Tiles
GET /api/dashboards/{dashboard_id}/data
Executes queries for all tiles in the dashboard.
Response (200 OK):
{
"id": "uuid",
"name": "Sales Dashboard",
"tiles": [
{
"id": "tile-uuid",
"title": "Revenue",
"tile_type": "chart",
"data": {
"columns": ["region", "revenue"],
"rows": [
{ "region": "North", "revenue": 150000 }
]
},
"error": null,
"executed_at": "2024-01-15T12:00:00Z"
}
]
}
Execute Single Tile
GET /api/dashboards/{dashboard_id}/tiles/{tile_id}/data
Response (200 OK):
{
"id": "tile-uuid",
"title": "Revenue",
"tile_type": "chart",
"data": {
"columns": ["region", "revenue"],
"rows": [
{ "region": "North", "revenue": 150000 }
]
},
"error": null,
"executed_at": "2024-01-15T12:00:00Z"
}
Snapshots
Snapshots cache query results for performance and public sharing.
Create Snapshot
POST /api/dashboards/{dashboard_id}/snapshot
Executes all tile queries and caches the results.
Response (201 Created):
{
"id": "snapshot-uuid",
"dashboard_id": "uuid",
"tile_data": { ... },
"execution_time_ms": 1234,
"executed_by": "user-id",
"trigger_type": "manual",
"created_at": "2024-01-15T12:00:00Z",
"expires_at": "2024-01-22T12:00:00Z"
}
Get Latest Snapshot
GET /api/dashboards/{dashboard_id}/snapshot/latest
Returns the most recent non-expired snapshot.
Interactivity
Drill Down into a Tile
POST /api/dashboards/{dashboard_id}/tiles/{tile_id}/drill
Resolves a drill step. The clicked value is bound to ${drill.value} and the cumulative WHERE chain to ${drill.where} before executing the tile's query.
Request Body:
{
"drill_path": [
{ "level": 0, "id_column": "country", "label_column": "country_name", "value": "ES" },
{ "level": 1, "id_column": "region", "label_column": "region_name", "value": "Madrid" }
],
"filter_values": { "date_range": { "start": "2026-01-01", "end": "2026-03-31" } },
"cross_filters": []
}
Response: Same shape as GET /tiles/{tile_id}/data.
Get Detail Rows for a Click
POST /api/dashboards/{dashboard_id}/tiles/{tile_id}/detail
Runs the tile's detail_query, substituting ${detail.where} with a safely-quoted AND col1 = val1 AND col2 = val2 chain built from dimensions. Drill and cross-filter state are merged in automatically.
Request Body:
{
"dimensions": { "region": "Madrid", "category": "wholesale" },
"filter_values": {},
"cross_filters": [],
"max_rows": 5000
}
Response (200 OK):
{
"columns": ["order_id", "customer", "order_date", "amount"],
"rows": [
{ "order_id": "A-101", "customer": "Acme", "order_date": "2026-02-14", "amount": 4200 }
],
"row_count": 1
}
Export
Download Tile Data
POST /api/dashboards/{dashboard_id}/tiles/{tile_id}/export
Exports the tile data in the requested format. Honours current filter / drill / cross-filter state.
Request Body:
{
"format": "csv",
"filter_values": {},
"cross_filters": [],
"drill_path": []
}
format is one of csv or xlsx.
Response: Binary download (Content-Disposition: attachment; filename=...).
AI Analytics
Forecast a Tile's Series
POST /api/dashboards/{dashboard_id}/tiles/{tile_id}/forecast
Returns a Holt-Winters forecast extension for a line/area/bar tile.
Request Body:
{
"horizon": 6,
"confidence_level": 0.95,
"filter_values": {},
"cross_filters": [],
"drill_path": []
}
Response (200 OK):
{
"x": ["2026-06", "2026-07", "2026-08"],
"y": [128000.0, 131500.0, 129200.0],
"lower": [121000.0, 123100.0, 119400.0],
"upper": [135000.0, 139900.0, 139000.0],
"granularity": "month"
}
Explain a Tile
POST /api/dashboards/{dashboard_id}/tiles/{tile_id}/explain
Generates a natural-language summary of what the tile shows, anomaly-aware.
Response (200 OK):
{
"summary": "Revenue in Q1 is up 18% versus Q4, driven mainly by the Madrid region (+34%). Two anomalies flagged in February correspond to the Black Friday catch-up campaign.",
"highlights": ["Madrid +34%", "Feb anomalies tied to campaign"]
}
Ask the Dashboard a Question
POST /api/dashboards/{dashboard_id}/qa
Stateless Q&A over the data currently visible on the dashboard. No SQL is executed against your warehouse — the LLM only sees the rows the tiles already retrieved.
Request Body:
{
"question": "Which region grew fastest last month?",
"filter_values": {},
"cross_filters": [],
"tile_focus": ["tile-uuid-1"]
}
tile_focus is optional; when omitted, the service summarises all tiles ordered by grid position (capped at 12).
Response (200 OK):
{
"answer": "Madrid grew 34% month over month, ahead of Barcelona (+12%) and Valencia (+6%).",
"referenced_tiles": [
{ "tile_id": "uuid-1", "title": "Revenue by region", "why": "Provides month-over-month revenue per region." }
],
"suggested_followups": [
"What drove Madrid's growth?",
"How does this compare to last year?"
],
"answerable_from_visible": true
}
When the question can't be answered from visible data, answerable_from_visible is false and answer explains what's missing.
AI SQL Assist
Generate or Modify Tile SQL
POST /api/dashboards/{dashboard_id}/tiles/{tile_id}/generate-sql
Section-specific AI writer. The query_kind query parameter selects which prompt block the backend builds — each one is purpose-built for a specific tile transformation.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
prompt | string | required | What the user wants (free-form). |
existing_query | string | "" | Current SQL to modify. Empty = generate from scratch. |
query_kind | enum | main | One of main, detail, comparison, drill, cross_filter. |
main_query | string | "" | Used by detail / comparison / drill / cross_filter — the tile's main aggregating query, passed as context. |
datasource_id | UUID | (tile's) | Override for unsaved tiles. |
catalog | string | (tile's) | Override for unsaved tiles. |
schema | string | (tile's) | Override for unsaved tiles. |
Response (200 OK):
{
"sql": "SELECT ${drill.field} AS dim, SUM(metric) AS total FROM (...) AS base WHERE 1=1 ${drill.where} GROUP BY ${drill.field}",
"suggested_levels": [
{ "field": "country", "label": "Country" },
{ "field": "region", "label": "Region" },
{ "field": "store", "label": "Store" }
],
"warnings": []
}
suggested_levels is returned for query_kind=drill (the levels the LLM proposed for the hierarchy). warnings is returned for query_kind=cross_filter (advisories from the audit). Both fields are omitted when not applicable.
Kind-specific behaviour:
main— generic SQL generation against the schema. No structured output.detail— generate thedetail_querymirroringmain_query's dimensions, with${detail.where},LIMIT 5000, no aggregates.comparison— rewritemain_queryto add a<metric>_prevcolumn using${range.prev_start}/${range.prev_end}with strict-<semantics onprev_end.drill— rewritemain_queryas a hierarchy with${drill.field}/${drill.where}, returnsuggested_levelsso the frontend can populatechart_config.drilldown.levelsin one click.cross_filter— auditmain_queryfor compatibility (UNION, aliases, schema-qualified columns), returnwarnings. SQL is unchanged unless a fix is suggested.
Checkpoints
User-named snapshots of a dashboard's configuration (pages, tiles, filters, settings). Pair with in-browser undo/redo: undo handles small mistakes; checkpoints handle "go back to last Tuesday before the redesign". Owner-only.
List Checkpoints
GET /api/dashboards/{dashboard_id}/checkpoints
Response (200 OK):
[
{
"id": "uuid",
"dashboard_id": "uuid",
"name": "v2 — before margin breakdown",
"description": "Last clean version before Q2 redesign",
"created_by": "user-id",
"created_at": "2026-05-17T12:00:00Z"
}
]
Create Checkpoint
POST /api/dashboards/{dashboard_id}/checkpoints
Request Body:
{
"name": "v2.1 final",
"description": "Locked in for end-of-quarter review"
}
Captures the dashboard's current pages + tiles + settings + filters as a JSON snapshot.
Restore Checkpoint
POST /api/dashboards/{dashboard_id}/checkpoints/{checkpoint_id}/restore
Replaces the dashboard's pages + tiles + dashboard-level settings with the checkpoint's snapshot. Cached query results (dashboard_snapshots) and public share tokens are preserved.
Response: { "status": "restored" }
Delete Checkpoint
DELETE /api/dashboards/{dashboard_id}/checkpoints/{checkpoint_id}
Response: 204 No Content
Export / Import
Move a dashboard between Qry instances. References to datasources are promoted to logical names so the importer can map them onto the target instance's datasources.
Export Dashboard Configuration
GET /api/dashboards/{dashboard_id}/export-config
Response: a downloadable .qry-dashboard.json file with Content-Disposition: attachment. Body shape:
{
"schema_version": 1,
"dashboard": { "name": "Sales", "layout": {...}, "filters": [...] },
"pages": [{ "_ref": "page_0", "name": "Overview", "page_order": 0 }],
"tiles": [
{
"title": "Revenue",
"tile_type": "kpi",
"_page_ref": "page_0",
"_datasource_ref": "ds_0",
"query": "SELECT SUM(amount) FROM orders WHERE ...",
"chart_config": {...}
}
],
"required_datasources": [
{ "ref": "ds_0", "name": "Primary warehouse", "type": "postgresql" }
]
}
Owner-only. Identifiers (id, dashboard_id, user_id, workspace_id, share_token) are stripped; cached snapshots are not included.
Import Preflight
POST /api/dashboards/import/preflight
Validate a bundle without committing. The UI uses this to drive the datasource-mapping step.
Request Body:
{
"bundle": { /* parsed export JSON */ }
}
Response (200 OK):
{
"name": "Sales",
"required_datasources": [
{ "ref": "ds_0", "name": "Primary warehouse", "type": "postgresql" }
],
"page_count": 2,
"tile_count": 9,
"filter_count": 1
}
Import Dashboard
POST /api/dashboards/import
Persist the bundle as a new dashboard owned by the caller. Each _datasource_ref in the bundle is rewritten to a concrete UUID via the supplied mapping; the importer verifies the caller has access to every target datasource.
Request Body:
{
"bundle": { /* parsed export JSON */ },
"datasource_mapping": {
"ds_0": "fda22d70-0000-4000-8000-000000000001"
},
"name": "Sales (imported)",
"workspace_id": null
}
Response (201 Created):
{ "id": "uuid-of-new-dashboard", "name": "Sales (imported)" }
Common 400 reasons: incomplete mapping (a required ref is missing), unsupported schema_version, access denied to a mapped datasource.
Chart Configuration Reference
Standard Charts (bar, line, area)
{
"xField": "category_column",
"yField": "value_column",
"seriesField": "group_column",
"showLegend": true,
"stacked": false,
"smooth": false,
"horizontal": false
}
Pie/Funnel Charts
{
"nameField": "category_column",
"valueField": "value_column",
"showLegend": true
}
Heatmap
{
"xField": "x_category",
"yField": "y_category",
"valueField": "intensity_value"
}
Sankey/Chord
{
"sourceField": "from_node",
"targetField": "to_node",
"valueField": "flow_value"
}
Treemap
{
"nameField": "label",
"valueField": "size",
"categoryField": "group"
}
Gauge
{
"valueField": "metric_value",
"min": 0,
"max": 100
}
Sunburst
{
"pathFields": ["country", "region", "store"],
"valueField": "revenue"
}
Boxplot
{
"categoryField": "department",
"valueField": "salary"
}
Candlestick
{
"dateField": "trading_day",
"openField": "open",
"highField": "high",
"lowField": "low",
"closeField": "close"
}
KPI Tile
{
"valueField": "metric_column",
"prefix": "$",
"suffix": "M",
"decimals": 1,
"comparisonField": "metric_prev_column",
"compareLabel": "vs prev period",
"lowerIsBetter": false,
"showSparkline": true,
"sparklineField": "metric_column"
}
Table Tile
{
"zebra": true,
"compact": false,
"pageSize": 10
}
Tile-Level Interactivity (any tile type)
{
"detail_query": "SELECT * FROM orders WHERE 1=1 ${detail.where} ${drill.where}",
"drill_levels": [
{ "id_column": "country", "label_column": "country_name" },
{ "id_column": "region", "label_column": "region_name" }
],
"cross_filter_enabled": true
}
detail_queryenables the click-to-detail modal. Must reference${detail.where}(save-time validation rejects detail queries without it). Also supports${drill.where}and any dashboard filter placeholders.drill_levelsenables drill-down. Resolved server-side viaPOST /drill.cross_filter_enabledlets this tile both emit and react to cross-filter clicks.
Error Responses
400 Bad Request
{
"detail": "Dashboard name is required"
}
404 Not Found
{
"detail": "Dashboard not found or access denied"
}
500 Internal Server Error
{
"detail": "Failed to execute dashboard queries"
}