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 (and WITH … SELECT). No INTO, no FOR UPDATE, nothing that writes.
  • FROM and JOIN may name only tables from the schema, or a WITH name defined in the same query. A WITH name 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.