Analytics queries API
Run read-only SQL over your data and connected sources, read the schema and manage indexes.
Analytics lets you ask questions of your data with SQL. This API describes what can be queried and runs read-only queries, either over your workspace or over an external database you have connected. Reports and dashboards are built in the app on top of the same queries.
Endpoints
| Method | Path | Permission | What it does |
|---|---|---|---|
GET |
/orgs/{orgId}/insights/schema |
insights:query |
Tables and columns you can query |
POST |
/orgs/{orgId}/insights/query |
insights:query |
Run a SELECT |
POST |
/orgs/{orgId}/insights/nl-query |
insights:query |
Turn a plain-language question into SQL (not run) |
GET |
/orgs/{orgId}/insights/storage |
insights:query |
Size per table |
GET |
/orgs/{orgId}/insights/tables/{table}/indexes |
insights:query |
List a table's indexes |
POST |
/orgs/{orgId}/insights/tables/{table}/indexes |
insights:manage |
Add an index |
DELETE |
/orgs/{orgId}/insights/tables/{table}/indexes/{column} |
insights:manage |
Drop an index |
GET |
/orgs/{orgId}/insights/datasources |
insights:query |
List connected external sources |
GET |
/orgs/{orgId}/insights/datasources/{dsId}/schema |
insights:query |
An external source's tables |
POST |
/orgs/{orgId}/insights/datasources/{dsId}/query |
insights:query |
Run a SELECT on an external source |
External sources are connected in the app (Analytics › Data sources) because they involve passwords.
What you can query
Every record type is a table named by its key (invoices, customers, …). Each table has an id, a created_at, and a column for every field of that type that holds a single value. List-type fields (refs, multiselect) are not columns. A ref field is a column holding the other record's id, so you join with ordinary SQL:
SELECT c.name, SUM(i.amount) AS open_total
FROM invoices i JOIN customers c ON c.id = i.customer
WHERE i.status = 'open'
GROUP BY c.name
ORDER BY open_total DESC
Some other app data can be queried too (for example queue items and calendar events). GET /insights/schema lists exactly what you may query.
Your permissions are applied for you. Records you may not read do not appear, and fields you may not read come back empty (NULL).
GET /orgs/{orgId}/insights/schema
{
"data": {
"tables": [
{
"name": "invoices",
"label": "Invoice",
"columns": [
{ "name": "id", "type": "ref" },
{ "name": "created_at", "type": "datetime" },
{ "name": "number", "type": "string" },
{ "name": "customer", "type": "ref", "ref": "customers" },
{ "name": "amount", "type": "currency", "description": "Invoice total" }
]
}
]
}
}
system: true marks tables that are not record types. ref names the table a reference column points at.
POST /orgs/{orgId}/insights/query
| Field | Type | Required | Notes |
|---|---|---|---|
sql |
string | Yes | One read-only SELECT, or UNION, INTERSECT or EXCEPT of selects |
params |
object of strings | No | Values for {{name}} placeholders |
curl -s -X POST https://axisiq.co/api/v1/orgs/$ORG/insights/query \
-H "Authorization: Bearer $AXIS_KEY" -H "Content-Type: application/json" \
-d '{"sql": "SELECT status, COUNT(*) AS n FROM invoices GROUP BY status"}'
{
"data": {
"columns": ["status", "n"],
"rows": [["open", 17], ["paid", 40]],
"truncated": false
}
}
rows are arrays in column order. At most 1,000 rows are returned; truncated is true when there were more, so aggregate in SQL rather than paging. A query is stopped after about 5 seconds.
Rules
- A single statement. No
;separating several statements. - Only
SELECT(andWITH … SELECT). NoINTO, noFOR UPDATE, nothing that writes. FROMandJOINmay name only tables from the schema, or aWITHname defined in the same query. AWITHname cannot reuse a table's name.- No names qualified by a database or schema.
- No comments (
--,/* */), no backslashes, no double-quoted strings. Use'single quotes'for text.
Variables. {{name}} and {{name=default}} in the SQL are replaced from params before the query is checked. A plain number is inserted as a number and anything else as a quoted string. A variable with no value and no default is an error. Up to 64 variables per query, each value up to 256 characters.
{ "sql": "SELECT COUNT(*) FROM invoices WHERE issue_date >= '{{from=2026-01-01}}'", "params": { "from": "2026-10-01" } }
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR |
The SQL does not parse, breaks a rule above, names an unknown table or column, or the query is rejected for another reason. The message says why |
| 403 | FORBIDDEN |
The role lacks insights:query |
POST /orgs/{orgId}/insights/nl-query
Body { "prompt": "open invoices by customer this quarter" }. Returns { "sql": "SELECT …" }. The SQL is not run. Review it and send it to /insights/query. Answers 503 AGENT_DISABLED when AI assistance is unavailable.
Storage and indexes
GET /insights/storage returns per-table rows, data_bytes and index_bytes plus total_rows, total_data_bytes and total_index_bytes.
Indexes make lookups on a column faster. GET …/indexes returns { "indexes": [ { "column": "status", "unique": false } ] }. POST …/indexes with { "column": "status" } adds a plain index (201, { "created": true }) and DELETE …/indexes/{column} removes one ({ "deleted": true }). Unique indexes come from fields marked unique and cannot be created or dropped here (400 VALIDATION_ERROR). Only record types can be indexed.
External data sources
GET /insights/datasources returns { "datasources": [ { "id", "name", "provider", "status", … } ] }. Rely only on those four fields; passwords and keys are never returned. provider is mysql, postgres, mongodb, hana or api.
GET …/datasources/{dsId}/schema returns { "tables": [ … ] } in the same shape as the workspace schema. POST …/datasources/{dsId}/query takes the same sql and params and returns the same result shape. Queries run read-only. Permissions on your own record fields do not apply to external data, because it is not stored in AxisIQ. A failing or unreachable source answers 502 DATASOURCE_ERROR.
Related
- Functions SDK:
axis.analytics.query - Records
- In the product: Analytics