> ## Documentation Index
> Fetch the complete documentation index at: https://docs.roark.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Query analytics

> Ask Roark for aggregate numbers over your calls and metrics from code, an agent, or the CLI: the same data behind your dashboards

The analytics API returns the numbers behind your [dashboards](/documentation/observability/dashboards) without opening the dashboard: average sentiment this week, calls per day, task completion by agent, a pass rate against last month. Send a query, get back a total and a per-interval series for each thing you asked about. Nothing is saved; to keep a query, add it to a dashboard as a [widget](#from-a-query-to-a-dashboard).

It is built for code and for AI agents alike. `GET /v1/analytics/schema` lists every metric you can query and what each one supports, so an agent can build a valid query on the first try.

## Quickstart

Calls per day over the last week, and the average sentiment score:

**Node**

```typescript theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
import Roark from '@roarkanalytics/sdk'

const client = new Roark({ bearerToken: process.env.ROARK_API_BEARER_TOKEN })

const { data } = await client.analytics.query({
  measures: [{ type: 'CONVERSATION' }, { metric: 'sentiment_score' }],
  timeRange: { type: 'LAST', value: 7, unit: 'DAYS' },
  interval: 'DAY',
})

for (const series of data.series) console.log(series.metric ?? 'calls', series.value)
```

**Python**

```python theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
import os
from roark_analytics import Roark

client = Roark(bearer_token=os.environ["ROARK_API_BEARER_TOKEN"])

result = client.analytics.query(
    measures=[{"type": "CONVERSATION"}, {"metric": "sentiment_score"}],
    time_range={"type": "LAST", "value": 7, "unit": "DAYS"},
    interval="DAY",
)

for series in result.data.series:
    print(series.metric or "calls", series.value)
```

**CLI**

```bash theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
roark analytics query --data '{
  "measures": [{ "type": "CONVERSATION" }, { "metric": "sentiment_score" }],
  "timeRange": { "type": "LAST", "value": 7, "unit": "DAYS" },
  "interval": "DAY"
}'
```

**cURL**

```bash theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
curl -X POST https://api.roark.ai/v1/analytics/query \
  -H "Authorization: Bearer $ROARK_API_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "measures": [{ "type": "CONVERSATION" }, { "metric": "sentiment_score" }],
    "timeRange": { "type": "LAST", "value": 7, "unit": "DAYS" },
    "interval": "DAY"
  }'
```

The key needs the `metric:read` permission. A read-only key is enough: a query never changes anything.

***

## What you can query

Read the schema first. It lists the project's metrics by slug, each with its output type, its default aggregation and the aggregations it supports, plus every filter and breakdown type and the custom call properties seen in the project.

```bash theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
roark analytics schema get
```

```json theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
{
  "data": {
    "metrics": [
      { "slug": "task_completion", "name": "Task Completion", "outputType": "SCALE", "defaultAggregation": "AVG", "aggregations": ["AVG", "MIN", "MAX", "SUM", "MEDIAN", "PERCENTILE", "COUNT", "PASS_RATE"] },
      { "slug": "call_outcome", "name": "Call Outcome", "outputType": "CLASSIFICATION", "defaultAggregation": "COUNT", "aggregations": ["COUNT", "PASS_RATE"] }
    ],
    "filters": [{ "type": "AGENT_PROPERTY", "description": "Conversations the agent took part in. Value: an agent id …" }],
    "callProperties": [{ "name": "region", "valueType": "STRING", "isSystem": false }],
    "intervals": ["HOUR", "DAY", "WEEK", "MONTH"]
  }
}
```

The [MCP server](/documentation/sdks/mcp-server) exposes the same two operations, so an assistant connected to Roark can read the schema and run queries on its own.

***

## Measures

A query has one to ten **measures**: the things to aggregate.

| Measure | What it counts | Default aggregation |
| :- | :- | :- |
| `{ "metric": "<slug>" }` | A metric's values | By output type: `AVG` for numbers and scales, `COUNT` split by value for classifications and text, `AVG` (the share of true) for booleans, `PASS_RATE` for thresholds |
| `{ "type": "CONVERSATION" }` | Conversations (calls and chats) | `COUNT` |
| `{ "type": "UPTIME" }` | Health-check probe outcomes | `PASS_RATE` (the up-rate) |

Set `aggregation` to choose another: `AVG`, `MIN`, `MAX`, `SUM`, `MEDIAN`, `PERCENTILE` (with `percentile`, e.g. `95`), `COUNT` or `PASS_RATE`. The schema lists which ones each metric supports, and a query asking for one it doesn't support returns `400` saying which to use instead.

* **Per call first.** Metric values are folded to one value per call before they are aggregated (`distinctBy: CALL`), so a long call with many scored segments doesn't outweigh a short one. Set `distinctBy: NONE` to aggregate every value.
* **Pass rates.** `PASS_RATE` is the share of calls that meet a condition, from 0 to 1: `{ "metric": "response_time", "aggregation": "PASS_RATE", "passOperator": "LESS_THAN", "passValue": 800 }`. A [threshold](/documentation/metrics/thresholds) metric already carries its condition, so querying it by slug gives its pass rate directly. `sampleSize` is the number of calls evaluated; `0` means no data, not 0%.
* **Units.** Values come back in the metric's display units.

***

## Time windows

`timeRange` picks the window. It defaults to the last 30 days, and covers at most two years.

| `type` | Example |
| :- | :- |
| `LAST` | `{ "type": "LAST", "value": 7, "unit": "DAYS" }` (`DAYS`, `WEEKS`, `MONTHS`) |
| `CALENDAR_ALIGNED` | `{ "type": "CALENDAR_ALIGNED", "period": "THIS_MONTH" }` (`TODAY`, `YESTERDAY`, `THIS_WEEK`, `LAST_WEEK`, `THIS_MONTH`, `LAST_MONTH`, `THIS_YEAR`, `LAST_YEAR`) |
| `FIXED` | `{ "type": "FIXED", "from": "2026-09-01T00:00:00Z", "to": "2026-10-01T00:00:00Z" }` |
| `SINCE` | `{ "type": "SINCE", "from": "2026-10-01T00:00:00Z" }` |

`timezone` (an IANA name such as `America/New_York`, default `UTC`) decides where days, weeks and calendar periods start. `interval` (`HOUR`, `DAY`, `WEEK`, `MONTH`) sets the bucket size; leave it out and it is chosen from the window's length. A window that would produce more than 500 buckets steps to a coarser interval. Set `includeBuckets: false` to get totals only.

***

## Filters, breakdowns and comparisons

**Filters** narrow what is counted. Top-level `filters` apply to every measure; a measure's own `filters` add to them.

```json theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
"filters": [
  { "type": "MODALITY", "value": "call" },
  { "type": "AGENT_PROPERTY", "value": "<agent-id>" },
  { "type": "CALL_PROPERTY", "property": "region", "value": "emea" },
  { "type": "METRIC_PROPERTY", "property": "participantRole", "value": "AGENT" }
]
```

`operator` defaults to `EQUALS`; the others are `NOT_EQUALS`, `GREATER_THAN`, `GREATER_THAN_OR_EQUAL_TO`, `LESS_THAN`, `LESS_THAN_OR_EQUAL_TO`, `CONTAINS` and `STARTS_WITH`. The schema lists every filter type and the properties each accepts.

**Breakdowns** split a measure into one series per value. `{ "metric": "sentiment_score", "breakdown": { "type": "AGENT_PROPERTY" } }` returns sentiment per agent; `{ "type": "CALL_PROPERTY", "property": "direction" }` splits by inbound and outbound. The 50 largest series are returned, and `omittedSeries` counts the rest.

**Comparisons**:

* `comparisonMode: "PREVIOUS_PERIOD"` adds the preceding window of the same length to every series (`comparisonValue`, and per bucket).
* `comparisonGroups` runs every measure once per named filter set, for example one group per agent or per region (2 to 5 groups). While groups are set, breakdowns are ignored: each group is one series.

***

## The response

```json theme={"theme":{"light":"everforest-light","dark":"everforest-dark"}}
{
  "data": {
    "from": "2026-10-14T09:00:00.000Z",
    "to": "2026-10-21T09:00:00.000Z",
    "interval": "DAY",
    "timezone": "UTC",
    "series": [
      {
        "measureIndex": 1,
        "type": "METRIC",
        "metric": "sentiment_score",
        "aggregation": "AVG",
        "breakdownValue": null,
        "comparisonGroup": null,
        "value": 0.62,
        "sampleSize": null,
        "comparisonValue": null,
        "buckets": [{ "date": "2026-10-14T00:00:00Z", "value": 0.58 }]
      }
    ],
    "omittedSeries": 0
  }
}
```

There is one series per measure, or per breakdown value or comparison group. `measureIndex` points back to the measure in your request. `value` is the aggregate over the whole window, and `buckets` holds the per-interval values.

A query the analytics store can't answer returns `400` naming the part it can't, never unfiltered numbers. An unknown metric slug returns `404`.

***

## From a query to a dashboard

A dashboard widget is a query plus a name, a chart type and a size. Once a query answers the question, save it as a widget with [`POST /v1/dashboard/{dashboardId}/widget`](/documentation/observability/dashboards#dashboards-over-the-api), or declare the dashboard in [config as code](/documentation/config-as-code/dashboards).

> **[Dashboards](/documentation/observability/dashboards)**
>
> Save queries as widgets and read them back over the API

> **[Dashboards as code](/documentation/config-as-code/dashboards)**
>
> Define dashboards in YAML next to the rest of your config


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.