# Reports

## List report datasets

**get** `/v2/reports/datasets`

Returns the reporting catalog: every dataset available to your workspace with its metrics and attributes.

This is the discovery endpoint for the reporting API — read it before building query payloads:

- **Metrics** carry the `metric` IDs accepted by `POST /v2/reports/query`, plus each metric's `allowedAggregations`, unit, and whether it supports office-hours restriction and period comparison.
- **Attributes** carry the `fieldId`s accepted in filter rules and the attribute IDs accepted as `groupBy` / `segmentBy`, plus each attribute's `allowedOperators` and value type. Attributes with `staticOptions` list their full value set inline; other filterable attributes resolve values via `POST /v2/reports/filter-values`.

The catalog includes your workspace's custom conversation/ticket attributes where applicable, so it can differ between workspaces.

### Version Availability

This endpoint is only available in API version 2026-01-01.nova and newer, and only for workspaces with the Reports product enabled (404 otherwise).

### Header Parameters

- `"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"`

  - `"2026-01-01.nova"`

  - `"2025-12-12.clover"`

### Returns

- `data: array of ReportDataset`

  - `id: string`

  - `attributes: array of ReportAttribute`

    - `id: string`

      Attribute ID — use as `fieldId` in filter rules and as `groupBy` / `segmentBy` in query requests.

    - `allowedOperators: array of "is" or "is_not" or "in" or 10 more`

      - `"is"`

      - `"is_not"`

      - `"in"`

      - `"not_in"`

      - `"contains"`

      - `"not_contains"`

      - `"gte"`

      - `"lte"`

      - `"between"`

      - `"exists"`

      - `"not_exists"`

      - `"is_member_of"`

      - `"is_not_member_of"`

    - `category: string`

    - `description: string`

    - `name: string`

    - `semantics: "current_state" or "historical_action" or "historical_snapshot" or "dynamic_metric_mapped"`

      Whether the filter uses the latest mutable state, an immutable historical action/snapshot, or a metric-dependent mapping.

      - `"current_state"`

      - `"historical_action"`

      - `"historical_snapshot"`

      - `"dynamic_metric_mapped"`

    - `supportsFilter: boolean`

      Whether the attribute can be used in `filters` rules.

    - `supportsGroupBy: boolean`

      Whether the attribute can be used as `groupBy` / `segmentBy`.

    - `supportsValueLookup: boolean`

      Whether `POST /v2/reports/filter-values` can list this attribute's values.

    - `valueType: "string" or "number" or "boolean" or 3 more`

      - `"string"`

      - `"number"`

      - `"boolean"`

      - `"date"`

      - `"enum"`

      - `"id"`

    - `multiValue: optional boolean`

    - `staticOptions: optional array of ReportAttributeOption`

      Fixed value set for enum-like attributes. Attributes without static options resolve values via `POST /v2/reports/filter-values`.

      - `label: string`

      - `value: string`

  - `description: string`

  - `metrics: array of ReportMetric`

    - `id: string`

      Metric ID — use as `metric` in query requests.

    - `allowedAggregations: array of "count" or "sum" or "avg" or 6 more`

      - `"count"`

      - `"sum"`

      - `"avg"`

      - `"median"`

      - `"min"`

      - `"max"`

      - `"range"`

      - `"percentile"`

      - `"value"`

    - `description: string`

    - `name: string`

    - `supportedFilterAttributeIds: array of string`

      Attribute IDs this specific metric accepts in filter expressions.

    - `supportedGroupByDimensions: array of string`

      Attribute IDs this specific metric accepts as `groupBy` or `segmentBy`.

    - `supportedViews: array of "standard" or "hourly_heatmap" or "sankey"`

      Which `view` values `POST /v2/reports/query` accepts for this metric. Most metrics support `standard` and `hourly_heatmap`; conversation-flow metrics are `sankey`-only.

      - `"standard"`

      - `"hourly_heatmap"`

      - `"sankey"`

    - `supportsOfficeHours: boolean`

    - `supportsPeriodComparison: boolean`

    - `type: "count" or "percentage" or "duration" or "number"`

      - `"count"`

      - `"percentage"`

      - `"duration"`

      - `"number"`

    - `unit: "count" or "percentage" or "duration_ms" or "number"`

      - `"count"`

      - `"percentage"`

      - `"duration_ms"`

      - `"number"`

    - `dataAvailableFromIso: optional string`

      Local calendar date (ISO 8601) from which this specific metric has data, when it differs from the global reporting floor. Windows ending before this date are rejected for this metric.

    - `isDefaultVariant: optional boolean`

      Whether this variant is the default selection for its `variantGroupId`.

    - `variantGroupId: optional string`

      Metrics sharing a `variantGroupId` are timestamp variants of one logical metric (same display `name`); each measures the same population at a different timestamp. Query the concrete metric `id` to pick a variant.

    - `variantId: optional string`

      Stable identifier for this variant within its group (e.g. `created_at`, `started_at`).

    - `variantLabel: optional string`

      Human-readable variant label for pickers (e.g. `Created at`, `Started at`).

  - `name: string`

- `object: "list"`

  - `"list"`

### Example

```http
curl https://do.featurebase.app/v2/reports/datasets \
    -H "Authorization: Bearer $FEATUREBASE_API_KEY"
```

#### Response

```json
{
  "data": [
    {
      "id": "conversations",
      "attributes": [
        {
          "id": "conversation.channel",
          "allowedOperators": [
            "in"
          ],
          "category": "conversation",
          "description": "description",
          "name": "Channel",
          "semantics": "current_state",
          "supportsFilter": true,
          "supportsGroupBy": true,
          "supportsValueLookup": true,
          "valueType": "string",
          "multiValue": true,
          "staticOptions": [
            {
              "label": "label",
              "value": "value"
            }
          ]
        }
      ],
      "description": "description",
      "metrics": [
        {
          "id": "new_conversations",
          "allowedAggregations": [
            "count"
          ],
          "description": "description",
          "name": "New conversations",
          "supportedFilterAttributeIds": [
            "string"
          ],
          "supportedGroupByDimensions": [
            "string"
          ],
          "supportedViews": [
            "standard"
          ],
          "supportsOfficeHours": true,
          "supportsPeriodComparison": true,
          "type": "count",
          "unit": "count",
          "dataAvailableFromIso": "2026-07-12",
          "isDefaultVariant": true,
          "variantGroupId": "new_conversations",
          "variantId": "started_at",
          "variantLabel": "Started at"
        }
      ],
      "name": "Conversations"
    }
  ],
  "object": "list"
}
```

## Run a report query

**post** `/v2/reports/query`

Executes an ad-hoc reporting query: one metric + aggregation over a date range, with optional filters, grouping, segmentation and period-over-period comparison.

### Building a query

1. Pick a `metric` and `aggregation` from the datasets catalog (`GET /v2/reports/datasets`). Counts use `count`; duration metrics support `sum`, `avg`, `median`, `min`, `max`, `range` and `percentile` (pass `percentile: 95` for p95); rate metrics use `value`.
1. Set the reporting window with `startDate` / `endDate` (ISO 8601) and a `granularity` (`hour`, `day`, `week`, `month`) for the returned time series. Reporting data is available from **July 12, 2026**. Windows ending earlier are rejected; crossing windows are clamped to that boundary in the requested timezone.
1. Optionally narrow with `filters` — a rule (`{ "kind": "rule", "fieldId": "...", "operator": "in", "value": [...] }`) or an `and`/`or` group of rules. Attribute IDs and allowed operators come from the catalog.
1. Optionally break results down with `groupBy` (primary dimension) and `segmentBy` (secondary dimension).
1. Optionally pass `compareStartDate` / `compareEndDate` to get `previousValue` / `deltaPercent` alongside every data point.

### Top-N and Show Other

For high-cardinality breakdowns, cap the number of returned series:

- `topValuesLimit` keeps only the top **N** `groupBy` values; `segmentTopValuesLimit` does the same for `segmentBy`. Allowed values: **5, 7, 10, 15, 20**. `topValuesLimit` requires `groupBy`; `segmentTopValuesLimit` requires `segmentBy`.
- Members are ranked by the selected metric/aggregation over the **full filtered primary range**, so the ranked set is **stable across every time bucket** (a value that spikes in one bucket but is small overall does not enter the set). A comparison period reuses the **primary period's** ranked set — it is never re-ranked, so the legend stays identical period-over-period.
- `showOther` / `segmentShowOther` append a single synthetic bucket with id `__other__` and label `Other` that sums every value outside the top set. Because the values are summed, Show Other is only supported for **additive aggregations** (`count`, `sum`, `value`); it is rejected for `avg`, `median`, `min`, `max`, `range`, `percentile`, and for percentage metrics (whose ratios cannot be summed). `showOther` also requires `topValuesLimit` (and `segmentShowOther` requires `segmentTopValuesLimit`). Plain `topValuesLimit` (without Show Other) works with any aggregation.
- The `__other__` group/segment aggregates rows outside the selected top groups and **cannot be drilled into** (`POST /v2/reports/drill-in` rejects `dataPointFilters.groupValue` / `segmentValue` equal to `__other__`).

### Example

```json
{
  "metric": "new_conversations",
  "aggregation": "count",
  "startDate": "2026-07-12",
  "endDate": "2026-07-15",
  "granularity": "day",
  "groupBy": "conversation.channel",
  "filters": {
    "kind": "rule",
    "fieldId": "conversation.state",
    "operator": "is",
    "value": "closed"
  }
}
```

### Response shape

- `value` — the aggregate across the whole window
- `timeSeries` — one datum per granularity bucket
- `groupedData` / `segmentData` — present when `groupBy` / `segmentBy` were requested
- `flowData` — present for `view: "sankey"` (Overview conversation-flow metrics)
- `meta` — echo of the resolved metric, dataset, unit and aggregation

### Special views

- `view: "hourly_heatmap"` buckets by day-of-week × hour-of-day (use with volume metrics)
- `view: "sankey"` returns conversation flow edges (Overview metrics only)

### Version Availability

This endpoint is only available in API version 2026-01-01.nova and newer, and only for workspaces with the Reports product enabled (404 otherwise).

### Header Parameters

- `"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"`

  - `"2026-01-01.nova"`

  - `"2025-12-12.clover"`

### Body Parameters

- `aggregation: "count" or "sum" or "avg" or 6 more`

  Aggregation function applied to the metric. Each metric supports a subset of aggregations — see the `allowedAggregations` field in the `GET /v2/reports/datasets` catalog.

  - `"count"`

  - `"sum"`

  - `"avg"`

  - `"median"`

  - `"min"`

  - `"max"`

  - `"range"`

  - `"percentile"`

  - `"value"`

- `endDate: string`

  End of the reporting window (ISO 8601 date or datetime, inclusive). Windows ending before 2026-07-12 are rejected.

- `granularity: "hour" or "day" or "week" or "month"`

  Time bucket size for the returned time series.

  - `"hour"`

  - `"day"`

  - `"week"`

  - `"month"`

- `metric: string`

  Metric ID to query (e.g. `new_conversations`). Discover metric IDs via `GET /v2/reports/datasets`.

- `startDate: string`

  Start of the reporting window (ISO 8601 date or datetime, inclusive). Reporting data is available from 2026-07-12; crossing windows are clamped to that boundary.

- `compareEndDate: optional string`

  End of the comparison window. Must be paired with `compareStartDate`.

- `compareStartDate: optional string`

  Start of the comparison window for period-over-period deltas. Must be paired with `compareEndDate`.

- `filters: optional ReportFilterRule or ReportFilterExpression`

  Filter expression: a single rule, or an `and`/`or` group combining rules and nested groups. Attribute IDs and their allowed operators come from `GET /v2/reports/datasets`.

  - `ReportFilterRule object { fieldId, kind, operator, value }`

    - `fieldId: string`

      Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

    - `kind: "rule"`

      - `"rule"`

    - `operator: "is" or "is_not" or "in" or 10 more`

      Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

      - `"is"`

      - `"is_not"`

      - `"in"`

      - `"not_in"`

      - `"contains"`

      - `"not_contains"`

      - `"gte"`

      - `"lte"`

      - `"between"`

      - `"exists"`

      - `"not_exists"`

      - `"is_member_of"`

      - `"is_not_member_of"`

    - `value: optional string or number or boolean or array of string or number or boolean`

      Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

      - `string`

      - `number`

      - `boolean`

      - `array of string or number or boolean`

        - `string`

        - `number`

        - `boolean`

  - `ReportFilterExpression object { children, kind, op }`

    - `children: array of ReportFilterRule or object { children, kind, op }`

      Rules and/or nested groups of rules. Groups may nest up to 6 levels at runtime; 100 rules max per expression.

      - `ReportFilterRule object { fieldId, kind, operator, value }`

      - `ReportFilterGroup object { children, kind, op }`

        - `children: array of ReportFilterRule`

          - `fieldId: string`

            Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

          - `kind: "rule"`

          - `operator: "is" or "is_not" or "in" or 10 more`

            Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

          - `value: optional string or number or boolean or array of string or number or boolean`

            Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

        - `kind: "group"`

          - `"group"`

        - `op: "and" or "or"`

          Group operator: `and` (all match) or `or` (any match).

          - `"and"`

          - `"or"`

    - `kind: "group"`

      - `"group"`

    - `op: "and" or "or"`

      Group operator: `and` (all match) or `or` (any match).

      - `"and"`

      - `"or"`

- `groupBy: optional string`

  Attribute ID to group results by (e.g. `conversation.channel`). See `supportsGroupBy` in the catalog.

- `officeHoursOnly: optional boolean`

  Restrict time-based metrics to configured office hours. Only supported by some metrics (see `supportsOfficeHours` in the catalog).

- `percentile: optional number`

  Percentile (1-100) — required when `aggregation` is `percentile`.

- `segmentBy: optional string`

  Attribute ID for secondary segmentation within each group or time bucket.

- `segmentShowOther: optional boolean`

  When `true`, fold `segmentBy` values outside the top set into a synthetic `__other__` / `Other` segment. Only supported for additive aggregations (`count`, `sum`, `value`) and not for percentage metrics. The `__other__` segment cannot be drilled into. Requires `segmentBy` and `segmentTopValuesLimit`.

- `segmentTopValuesLimit: optional 5 or 7 or 10 or 2 more`

  Keep only the top N `segmentBy` values per series, ranked by the selected metric over the full filtered primary range. Under a time View-by the ranked segment set is identical across every bucket; under a dimension View-by the ranking is applied within each retained parent group. Allowed values: `5`, `7`, `10`, `15`, `20`. Requires `segmentBy`.

  - `5`

  - `7`

  - `10`

  - `15`

  - `20`

- `showOther: optional boolean`

  When `true`, append a single synthetic group with id `__other__` and label `Other` that sums the values of every `groupBy` value outside the top set. Only supported for additive aggregations (`count`, `sum`, `value`) and not for percentage metrics. The `__other__` group cannot be drilled into. Requires `groupBy` and `topValuesLimit`.

- `timezone: optional string`

  IANA timezone for date bucketing (e.g. `America/New_York`). Defaults to UTC.

- `topValuesLimit: optional 5 or 7 or 10 or 2 more`

  Keep only the top N `groupBy` (View-by) values, ranked by the selected metric/aggregation over the full filtered primary range. Allowed values: `5`, `7`, `10`, `15`, `20`. The ranked set is stable across every time bucket, and a comparison period reuses the primary period's ranked set (never re-ranked). Requires `groupBy`.

  - `5`

  - `7`

  - `10`

  - `15`

  - `20`

- `view: optional "standard" or "hourly_heatmap" or "sankey"`

  Result shape. `standard` returns a time series (plus grouped/segment data when requested), `hourly_heatmap` buckets by day-of-week × hour-of-day, `sankey` returns conversation flow data. Each metric lists its `supportedViews` in the `GET /v2/reports/datasets` catalog.

  - `"standard"`

  - `"hourly_heatmap"`

  - `"sankey"`

### Returns

- `meta: object { aggregation, datasetId, granularity, 5 more }`

  - `aggregation: "count" or "sum" or "avg" or 6 more`

    Aggregation function applied to the metric. Each metric supports a subset of aggregations — see the `allowedAggregations` field in the `GET /v2/reports/datasets` catalog.

    - `"count"`

    - `"sum"`

    - `"avg"`

    - `"median"`

    - `"min"`

    - `"max"`

    - `"range"`

    - `"percentile"`

    - `"value"`

  - `datasetId: string`

  - `granularity: string`

  - `metric: string`

  - `officeHoursOnly: boolean`

  - `unit: "count" or "percentage" or "duration_ms" or "number"`

    - `"count"`

    - `"percentage"`

    - `"duration_ms"`

    - `"number"`

  - `groupBy: optional string`

  - `segmentBy: optional string`

- `object: "report_query_result"`

  - `"report_query_result"`

- `value: number`

  Aggregated value across the whole reporting window.

- `deltaPercent: optional number`

  Percentage change vs the comparison window.

- `flowData: optional array of object { metricId, pathId, source, 7 more }`

  - `metricId: string`

  - `pathId: string`

  - `source: string`

  - `sourceLabel: string`

  - `target: string`

  - `targetLabel: string`

  - `value: number`

  - `colorKey: optional string`

  - `percentage: optional number`

  - `sortOrder: optional number`

- `groupedData: optional array of ReportGroupedDatum`

  - `group: string`

    Raw group value (e.g. an ID). Use `groupLabel` for display.

  - `value: number`

  - `groupLabel: optional string`

  - `previousValue: optional number`

  - `segments: optional array of ReportSegmentDatum`

    - `segment: string`

    - `value: number`

    - `previousValue: optional number`

    - `segmentLabel: optional string`

- `previousValue: optional number`

  Aggregated value for the comparison window, when requested.

- `segmentData: optional array of ReportSegmentDatum`

  - `segment: string`

  - `value: number`

  - `previousValue: optional number`

  - `segmentLabel: optional string`

- `table: optional object { columns, key, mode, 6 more }`

  - `columns: array of object { id, kind, label, 6 more }`

    - `id: string`

    - `kind: "dimension" or "metric" or "record_attribute"`

      - `"dimension"`

      - `"metric"`

      - `"record_attribute"`

    - `label: string`

    - `sortable: boolean`

    - `unit: "count" or "percentage" or "duration_ms" or 3 more`

      - `"count"`

      - `"percentage"`

      - `"duration_ms"`

      - `"number"`

      - `"text"`

      - `"datetime"`

    - `align: optional "left" or "right"`

      - `"left"`

      - `"right"`

    - `sticky: optional boolean`

    - `summary: optional "sum" or "weighted_rate" or "none"`

      - `"sum"`

      - `"weighted_rate"`

      - `"none"`

    - `valueType: optional string`

  - `key: string`

  - `mode: "aggregate" or "records"`

    - `"aggregate"`

    - `"records"`

  - `page: number`

  - `pageSize: number`

  - `rows: array of object { id, cells }`

    - `id: string`

    - `cells: map[object { display, denominator, numerator, 3 more } ]`

      - `display: string`

      - `denominator: optional number`

      - `numerator: optional number`

      - `raw: optional unknown`

      - `sortValue: optional string or number`

        - `string`

        - `number`

      - `value: optional unknown`

  - `totalRows: number`

  - `sort: optional object { columnId, direction }`

    - `columnId: string`

    - `direction: "asc" or "desc"`

      - `"asc"`

      - `"desc"`

  - `summaryRows: optional array of object { id, cells }`

    - `id: string`

    - `cells: map[object { display, denominator, numerator, 3 more } ]`

      - `display: string`

      - `denominator: optional number`

      - `numerator: optional number`

      - `raw: optional unknown`

      - `sortValue: optional string or number`

        - `string`

        - `number`

      - `value: optional unknown`

- `timeSeries: optional array of ReportTimeSeriesDatum`

  - `date: string`

    Time bucket start (ISO 8601).

  - `value: number`

  - `previousValue: optional number`

    Value for the same bucket in the comparison window, when requested.

  - `segments: optional array of ReportSegmentDatum`

    - `segment: string`

    - `value: number`

    - `previousValue: optional number`

    - `segmentLabel: optional string`

### Example

```http
curl https://do.featurebase.app/v2/reports/query \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $FEATUREBASE_API_KEY" \
    -d '{
          "aggregation": "count",
          "endDate": "2026-07-15",
          "granularity": "day",
          "metric": "new_conversations",
          "startDate": "2026-07-12",
          "groupBy": "conversation.channel",
          "percentile": 95,
          "segmentTopValuesLimit": 5,
          "timezone": "Europe/Tallinn",
          "topValuesLimit": 10,
          "view": "standard"
        }'
```

#### Response

```json
{
  "meta": {
    "aggregation": "count",
    "datasetId": "datasetId",
    "granularity": "granularity",
    "metric": "metric",
    "officeHoursOnly": true,
    "unit": "count",
    "groupBy": "groupBy",
    "segmentBy": "segmentBy"
  },
  "object": "report_query_result",
  "value": 0,
  "deltaPercent": 0,
  "flowData": [
    {
      "metricId": "metricId",
      "pathId": "pathId",
      "source": "source",
      "sourceLabel": "sourceLabel",
      "target": "target",
      "targetLabel": "targetLabel",
      "value": 0,
      "colorKey": "colorKey",
      "percentage": 0,
      "sortOrder": 0
    }
  ],
  "groupedData": [
    {
      "group": "group",
      "value": 0,
      "groupLabel": "groupLabel",
      "previousValue": 0,
      "segments": [
        {
          "segment": "segment",
          "value": 0,
          "previousValue": 0,
          "segmentLabel": "segmentLabel"
        }
      ]
    }
  ],
  "previousValue": 0,
  "segmentData": [
    {
      "segment": "segment",
      "value": 0,
      "previousValue": 0,
      "segmentLabel": "segmentLabel"
    }
  ],
  "table": {
    "columns": [
      {
        "id": "id",
        "kind": "dimension",
        "label": "label",
        "sortable": true,
        "unit": "count",
        "align": "left",
        "sticky": true,
        "summary": "sum",
        "valueType": "valueType"
      }
    ],
    "key": "key",
    "mode": "aggregate",
    "page": 0,
    "pageSize": 0,
    "rows": [
      {
        "id": "id",
        "cells": {
          "foo": {
            "display": "display",
            "denominator": 0,
            "numerator": 0,
            "raw": {},
            "sortValue": "string",
            "value": {}
          }
        }
      }
    ],
    "totalRows": 0,
    "sort": {
      "columnId": "columnId",
      "direction": "asc"
    },
    "summaryRows": [
      {
        "id": "id",
        "cells": {
          "foo": {
            "display": "display",
            "denominator": 0,
            "numerator": 0,
            "raw": {},
            "sortValue": "string",
            "value": {}
          }
        }
      }
    ]
  },
  "timeSeries": [
    {
      "date": "2026-07-15",
      "value": 0,
      "previousValue": 0,
      "segments": [
        {
          "segment": "segment",
          "value": 0,
          "previousValue": 0,
          "segmentLabel": "segmentLabel"
        }
      ]
    }
  ]
}
```

## Look up report filter values

**post** `/v2/reports/filter-values`

Returns the available values for a filterable attribute — use it to build valid `filters` rules for `POST /v2/reports/query`.

Works for attributes whose values live in your workspace data (tags, teammates, teams, companies, plans, countries, ...). Attributes with `staticOptions` in the catalog don't need this endpoint — their value set is already inline.

- `query` narrows results by search string
- `selectedValues` resolves labels for values you already hold (returned alongside search results)

### Example

```json
{ "fieldId": "conversation.tags", "query": "bill", "limit": 20 }
```

### Version Availability

This endpoint is only available in API version 2026-01-01.nova and newer, and only for workspaces with the Reports product enabled (404 otherwise).

### Header Parameters

- `"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"`

  - `"2026-01-01.nova"`

  - `"2025-12-12.clover"`

### Body Parameters

- `fieldId: string`

  Attribute ID to look up values for (must have `valueSource: "remote_search"` or static options in the catalog).

- `limit: optional number`

  Maximum number of options to return (1-50, default 20).

- `query: optional string`

  Optional search string to narrow the returned options.

- `selectedValues: optional array of string`

  Already-selected values to resolve labels for (returned alongside search results).

### Returns

- `data: array of ReportAttributeOption`

  - `label: string`

  - `value: string`

- `object: "list"`

  - `"list"`

### Example

```http
curl https://do.featurebase.app/v2/reports/filter-values \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $FEATUREBASE_API_KEY" \
    -d '{
          "fieldId": "conversation.tags",
          "query": "bill"
        }'
```

#### Response

```json
{
  "data": [
    {
      "label": "label",
      "value": "value"
    }
  ],
  "object": "list"
}
```

## Drill into report rows

**post** `/v2/reports/drill-in`

Returns the paginated row-level records behind a metric — either the whole reporting window, or one specific data point from a previous `POST /v2/reports/query` response.

Send the **same** `metric`, date range, `granularity`, `filters`, `groupBy` / `segmentBy` and `view` as the query you're drilling into, plus `dataPointFilters` selecting the data point:

- `timeBucket` — a `date` from the time series
- `groupValue` / `segmentValue` — a `group` / `segment` value from grouped data
- `dayOfWeek` + `hourOfDay` — a heatmap cell (`hourly_heatmap` view)
- `flowPathId` or `sourceNodeId` + `targetNodeId` — a flow edge (`sankey` view)

Pass `dataPointFilters: {}` to list all rows behind the metric for the window.

Results are paginated with `page` / `pageSize` (max 200 per page); `availableColumns` describes every column the dataset can return and `defaultColumnIds` the recommended subset. Rows are keyed by column ID; identity cells (teammates, contacts) are objects with `id` + `label`.

### Version Availability

This endpoint is only available in API version 2026-01-01.nova and newer, and only for workspaces with the Reports product enabled (404 otherwise).

### Header Parameters

- `"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"`

  - `"2026-01-01.nova"`

  - `"2025-12-12.clover"`

### Body Parameters

- `endDate: string`

  End of the reporting window (ISO 8601 date or datetime, inclusive). Windows ending before 2026-07-12 are rejected.

- `granularity: "hour" or "day" or "week" or "month"`

  Time bucket size for the returned time series.

  - `"hour"`

  - `"day"`

  - `"week"`

  - `"month"`

- `metric: string`

  Metric ID the drill-in belongs to.

- `startDate: string`

  Start of the reporting window (ISO 8601 date or datetime, inclusive). Reporting data is available from 2026-07-12; crossing windows are clamped to that boundary.

- `chartEditorId: optional string`

  Stable editor ID of the saved chart that launched this drill-in.

- `columns: optional array of string`

  Column IDs to include in each returned row. Omit for legacy full-width static rows; pass an empty array to use `defaultColumnIds`.

- `dataPointFilters: optional object { dayOfWeek, flowPathId, groupValue, 5 more }`

  Narrows the drill-in to one data point from a previous query (time bucket, group value, heatmap cell, or flow edge). Pass `{}` to list all underlying rows.

  - `dayOfWeek: optional number`

    Day of week (1 = Monday … 7 = Sunday) — for `hourly_heatmap` views.

  - `flowPathId: optional string`

    Flow path ID — for `sankey` views.

  - `groupValue: optional string`

    Group value to drill into (a `group` value from `groupedData`).

  - `hourOfDay: optional number`

    Hour of day (0-23) — for `hourly_heatmap` views.

  - `segmentValue: optional string`

    Segment value to drill into (a `segment` value from segment data).

  - `sourceNodeId: optional string`

    Flow source node ID — for `sankey` views.

  - `targetNodeId: optional string`

    Flow target node ID — for `sankey` views.

  - `timeBucket: optional string`

    Time bucket to drill into (a `date` value from the query time series).

- `filters: optional ReportFilterRule or ReportFilterExpression`

  Filter expression: a single rule, or an `and`/`or` group combining rules and nested groups. Attribute IDs and their allowed operators come from `GET /v2/reports/datasets`.

  - `ReportFilterRule object { fieldId, kind, operator, value }`

    - `fieldId: string`

      Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

    - `kind: "rule"`

      - `"rule"`

    - `operator: "is" or "is_not" or "in" or 10 more`

      Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

      - `"is"`

      - `"is_not"`

      - `"in"`

      - `"not_in"`

      - `"contains"`

      - `"not_contains"`

      - `"gte"`

      - `"lte"`

      - `"between"`

      - `"exists"`

      - `"not_exists"`

      - `"is_member_of"`

      - `"is_not_member_of"`

    - `value: optional string or number or boolean or array of string or number or boolean`

      Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

      - `string`

      - `number`

      - `boolean`

      - `array of string or number or boolean`

        - `string`

        - `number`

        - `boolean`

  - `ReportFilterExpression object { children, kind, op }`

    - `children: array of ReportFilterRule or object { children, kind, op }`

      Rules and/or nested groups of rules. Groups may nest up to 6 levels at runtime; 100 rules max per expression.

      - `ReportFilterRule object { fieldId, kind, operator, value }`

      - `ReportFilterGroup object { children, kind, op }`

        - `children: array of ReportFilterRule`

          - `fieldId: string`

            Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

          - `kind: "rule"`

          - `operator: "is" or "is_not" or "in" or 10 more`

            Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

          - `value: optional string or number or boolean or array of string or number or boolean`

            Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

        - `kind: "group"`

          - `"group"`

        - `op: "and" or "or"`

          Group operator: `and` (all match) or `or` (any match).

          - `"and"`

          - `"or"`

    - `kind: "group"`

      - `"group"`

    - `op: "and" or "or"`

      Group operator: `and` (all match) or `or` (any match).

      - `"and"`

      - `"or"`

- `groupBy: optional string`

- `officeHoursOnly: optional boolean`

- `page: optional number`

  Page number (1-based).

- `pageSize: optional number`

  Rows per page (1-200, default 25).

- `segmentBy: optional string`

- `selectedMetricId: optional string`

  A metric ID from the datasets catalog. Use when the originating chart aggregates several metrics to pick which one the drill-in follows. Defaults to `metric`.

- `selectedMetricRowId: optional string`

  Stable metric-row ID selected within the launching chart.

- `sort: optional array of object { direction, fieldId }`

  - `direction: "asc" or "desc"`

    - `"asc"`

    - `"desc"`

  - `fieldId: string`

    Column ID to sort by (an attribute ID or `metric.value`).

- `timezone: optional string`

- `view: optional "standard" or "hourly_heatmap" or "sankey"`

  Result shape. `standard` returns a time series (plus grouped/segment data when requested), `hourly_heatmap` buckets by day-of-week × hour-of-day, `sankey` returns conversation flow data. Each metric lists its `supportedViews` in the `GET /v2/reports/datasets` catalog.

  - `"standard"`

  - `"hourly_heatmap"`

  - `"sankey"`

### Returns

- `availableColumns: array of object { id, name, supportsSort, 2 more }`

  - `id: string`

    Column ID (an attribute ID or `metric.value`).

  - `name: string`

  - `supportsSort: boolean`

    Whether this column may be used in the drill-in sort request.

  - `valueType: string`

  - `category: optional string`

- `defaultColumnIds: array of string`

- `meta: object { datasetId, metricId, rawMetricId, 6 more }`

  - `datasetId: string`

  - `metricId: string`

  - `rawMetricId: string`

  - `reconciliation: object { kind, rowCount, status, 4 more }`

    - `kind: "rows" or "actions_across_conversations" or "active_hour_rate" or 2 more`

      - `"rows"`

      - `"actions_across_conversations"`

      - `"active_hour_rate"`

      - `"positive_ratings_over_eligible_ratings"`

      - `"missed_over_evaluated"`

    - `rowCount: number`

    - `status: "not_applicable" or "available" or "denominator_unavailable"`

      - `"not_applicable"`

      - `"available"`

      - `"denominator_unavailable"`

    - `denominatorHours: optional number`

    - `displayedRate: optional number`

    - `eligibleDenominatorTotal: optional number`

    - `rawNumeratorTotal: optional number`

  - `rowGrain: string`

  - `rowSemantics: "metric_rows" or "numerator_rows"`

    - `"metric_rows"`

    - `"numerator_rows"`

  - `sourceFact: string`

  - `emptyReason: optional "no_matching_rows" or "denominator_unavailable"`

    - `"no_matching_rows"`

    - `"denominator_unavailable"`

  - `metricRowId: optional string`

- `object: "report_drill_in_result"`

  - `"report_drill_in_result"`

- `page: number`

- `pageSize: number`

- `rows: array of map[string or number or boolean or 3 more]`

  One record per underlying row, keyed by column ID.

  - `string`

  - `number`

  - `boolean`

  - `array of string`

  - `ReportDrillInIdentityCell object { id, kind, label, 3 more }`

    - `id: string`

    - `kind: "identity"`

      - `"identity"`

    - `label: string`

    - `avatarUrl: optional string`

    - `color: optional string`

    - `provenance: optional "current" or "historical" or "fallback" or "unassigned"`

      - `"current"`

      - `"historical"`

      - `"fallback"`

      - `"unassigned"`

  - `ReportDrillInSourceDetailsCell object { items, kind }`

    - `items: array of object { label, id, type, url }`

      - `label: string`

      - `id: optional string`

      - `type: optional string`

      - `url: optional string`

    - `kind: "source_details"`

      - `"source_details"`

- `timezone: string`

- `total: number`

### Example

```http
curl https://do.featurebase.app/v2/reports/drill-in \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $FEATUREBASE_API_KEY" \
    -d '{
          "endDate": "2026-07-15",
          "granularity": "day",
          "metric": "new_conversations",
          "startDate": "2026-07-12",
          "view": "standard"
        }'
```

#### Response

```json
{
  "availableColumns": [
    {
      "id": "id",
      "name": "name",
      "supportsSort": true,
      "valueType": "valueType",
      "category": "category"
    }
  ],
  "defaultColumnIds": [
    "string"
  ],
  "meta": {
    "datasetId": "datasetId",
    "metricId": "metricId",
    "rawMetricId": "rawMetricId",
    "reconciliation": {
      "kind": "rows",
      "rowCount": 0,
      "status": "not_applicable",
      "denominatorHours": 0,
      "displayedRate": 0,
      "eligibleDenominatorTotal": 0,
      "rawNumeratorTotal": 0
    },
    "rowGrain": "rowGrain",
    "rowSemantics": "metric_rows",
    "sourceFact": "sourceFact",
    "emptyReason": "no_matching_rows",
    "metricRowId": "metricRowId"
  },
  "object": "report_drill_in_result",
  "page": 0,
  "pageSize": 0,
  "rows": [
    {
      "foo": "string"
    }
  ],
  "timezone": "timezone",
  "total": 0
}
```

## Domain Types

### Report Attribute

- `ReportAttribute object { id, allowedOperators, category, 9 more }`

  - `id: string`

    Attribute ID — use as `fieldId` in filter rules and as `groupBy` / `segmentBy` in query requests.

  - `allowedOperators: array of "is" or "is_not" or "in" or 10 more`

    - `"is"`

    - `"is_not"`

    - `"in"`

    - `"not_in"`

    - `"contains"`

    - `"not_contains"`

    - `"gte"`

    - `"lte"`

    - `"between"`

    - `"exists"`

    - `"not_exists"`

    - `"is_member_of"`

    - `"is_not_member_of"`

  - `category: string`

  - `description: string`

  - `name: string`

  - `semantics: "current_state" or "historical_action" or "historical_snapshot" or "dynamic_metric_mapped"`

    Whether the filter uses the latest mutable state, an immutable historical action/snapshot, or a metric-dependent mapping.

    - `"current_state"`

    - `"historical_action"`

    - `"historical_snapshot"`

    - `"dynamic_metric_mapped"`

  - `supportsFilter: boolean`

    Whether the attribute can be used in `filters` rules.

  - `supportsGroupBy: boolean`

    Whether the attribute can be used as `groupBy` / `segmentBy`.

  - `supportsValueLookup: boolean`

    Whether `POST /v2/reports/filter-values` can list this attribute's values.

  - `valueType: "string" or "number" or "boolean" or 3 more`

    - `"string"`

    - `"number"`

    - `"boolean"`

    - `"date"`

    - `"enum"`

    - `"id"`

  - `multiValue: optional boolean`

  - `staticOptions: optional array of ReportAttributeOption`

    Fixed value set for enum-like attributes. Attributes without static options resolve values via `POST /v2/reports/filter-values`.

    - `label: string`

    - `value: string`

### Report Attribute Option

- `ReportAttributeOption object { label, value }`

  - `label: string`

  - `value: string`

### Report Dataset

- `ReportDataset object { id, attributes, description, 2 more }`

  - `id: string`

  - `attributes: array of ReportAttribute`

    - `id: string`

      Attribute ID — use as `fieldId` in filter rules and as `groupBy` / `segmentBy` in query requests.

    - `allowedOperators: array of "is" or "is_not" or "in" or 10 more`

      - `"is"`

      - `"is_not"`

      - `"in"`

      - `"not_in"`

      - `"contains"`

      - `"not_contains"`

      - `"gte"`

      - `"lte"`

      - `"between"`

      - `"exists"`

      - `"not_exists"`

      - `"is_member_of"`

      - `"is_not_member_of"`

    - `category: string`

    - `description: string`

    - `name: string`

    - `semantics: "current_state" or "historical_action" or "historical_snapshot" or "dynamic_metric_mapped"`

      Whether the filter uses the latest mutable state, an immutable historical action/snapshot, or a metric-dependent mapping.

      - `"current_state"`

      - `"historical_action"`

      - `"historical_snapshot"`

      - `"dynamic_metric_mapped"`

    - `supportsFilter: boolean`

      Whether the attribute can be used in `filters` rules.

    - `supportsGroupBy: boolean`

      Whether the attribute can be used as `groupBy` / `segmentBy`.

    - `supportsValueLookup: boolean`

      Whether `POST /v2/reports/filter-values` can list this attribute's values.

    - `valueType: "string" or "number" or "boolean" or 3 more`

      - `"string"`

      - `"number"`

      - `"boolean"`

      - `"date"`

      - `"enum"`

      - `"id"`

    - `multiValue: optional boolean`

    - `staticOptions: optional array of ReportAttributeOption`

      Fixed value set for enum-like attributes. Attributes without static options resolve values via `POST /v2/reports/filter-values`.

      - `label: string`

      - `value: string`

  - `description: string`

  - `metrics: array of ReportMetric`

    - `id: string`

      Metric ID — use as `metric` in query requests.

    - `allowedAggregations: array of "count" or "sum" or "avg" or 6 more`

      - `"count"`

      - `"sum"`

      - `"avg"`

      - `"median"`

      - `"min"`

      - `"max"`

      - `"range"`

      - `"percentile"`

      - `"value"`

    - `description: string`

    - `name: string`

    - `supportedFilterAttributeIds: array of string`

      Attribute IDs this specific metric accepts in filter expressions.

    - `supportedGroupByDimensions: array of string`

      Attribute IDs this specific metric accepts as `groupBy` or `segmentBy`.

    - `supportedViews: array of "standard" or "hourly_heatmap" or "sankey"`

      Which `view` values `POST /v2/reports/query` accepts for this metric. Most metrics support `standard` and `hourly_heatmap`; conversation-flow metrics are `sankey`-only.

      - `"standard"`

      - `"hourly_heatmap"`

      - `"sankey"`

    - `supportsOfficeHours: boolean`

    - `supportsPeriodComparison: boolean`

    - `type: "count" or "percentage" or "duration" or "number"`

      - `"count"`

      - `"percentage"`

      - `"duration"`

      - `"number"`

    - `unit: "count" or "percentage" or "duration_ms" or "number"`

      - `"count"`

      - `"percentage"`

      - `"duration_ms"`

      - `"number"`

    - `dataAvailableFromIso: optional string`

      Local calendar date (ISO 8601) from which this specific metric has data, when it differs from the global reporting floor. Windows ending before this date are rejected for this metric.

    - `isDefaultVariant: optional boolean`

      Whether this variant is the default selection for its `variantGroupId`.

    - `variantGroupId: optional string`

      Metrics sharing a `variantGroupId` are timestamp variants of one logical metric (same display `name`); each measures the same population at a different timestamp. Query the concrete metric `id` to pick a variant.

    - `variantId: optional string`

      Stable identifier for this variant within its group (e.g. `created_at`, `started_at`).

    - `variantLabel: optional string`

      Human-readable variant label for pickers (e.g. `Created at`, `Started at`).

  - `name: string`

### Report Filter Expression

- `ReportFilterExpression object { children, kind, op }`

  - `children: array of ReportFilterRule or object { children, kind, op }`

    Rules and/or nested groups of rules. Groups may nest up to 6 levels at runtime; 100 rules max per expression.

    - `ReportFilterRule object { fieldId, kind, operator, value }`

      - `fieldId: string`

        Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

      - `kind: "rule"`

        - `"rule"`

      - `operator: "is" or "is_not" or "in" or 10 more`

        Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

        - `"is"`

        - `"is_not"`

        - `"in"`

        - `"not_in"`

        - `"contains"`

        - `"not_contains"`

        - `"gte"`

        - `"lte"`

        - `"between"`

        - `"exists"`

        - `"not_exists"`

        - `"is_member_of"`

        - `"is_not_member_of"`

      - `value: optional string or number or boolean or array of string or number or boolean`

        Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

        - `string`

        - `number`

        - `boolean`

        - `array of string or number or boolean`

          - `string`

          - `number`

          - `boolean`

    - `ReportFilterGroup object { children, kind, op }`

      - `children: array of ReportFilterRule`

        - `fieldId: string`

          Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

        - `kind: "rule"`

        - `operator: "is" or "is_not" or "in" or 10 more`

          Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

        - `value: optional string or number or boolean or array of string or number or boolean`

          Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

      - `kind: "group"`

        - `"group"`

      - `op: "and" or "or"`

        Group operator: `and` (all match) or `or` (any match).

        - `"and"`

        - `"or"`

  - `kind: "group"`

    - `"group"`

  - `op: "and" or "or"`

    Group operator: `and` (all match) or `or` (any match).

    - `"and"`

    - `"or"`

### Report Filter Rule

- `ReportFilterRule object { fieldId, kind, operator, value }`

  - `fieldId: string`

    Attribute ID to filter on (e.g. `conversation.channel`). Discover attribute IDs via `GET /v2/reports/datasets`.

  - `kind: "rule"`

    - `"rule"`

  - `operator: "is" or "is_not" or "in" or 10 more`

    Comparison operator. Each attribute supports a subset of operators — see the `allowedOperators` field in the `GET /v2/reports/datasets` catalog.

    - `"is"`

    - `"is_not"`

    - `"in"`

    - `"not_in"`

    - `"contains"`

    - `"not_contains"`

    - `"gte"`

    - `"lte"`

    - `"between"`

    - `"exists"`

    - `"not_exists"`

    - `"is_member_of"`

    - `"is_not_member_of"`

  - `value: optional string or number or boolean or array of string or number or boolean`

    Value to compare against. Scalar for `is`, `is_not`, `contains`, `not_contains`, `gte`, `lte` (ISO date strings for date attributes); non-empty array for `in`, `not_in`, `is_member_of`, `is_not_member_of`; 2-element array for `between`; omit for `exists` / `not_exists`.

    - `string`

    - `number`

    - `boolean`

    - `array of string or number or boolean`

      - `string`

      - `number`

      - `boolean`

### Report Grouped Datum

- `ReportGroupedDatum object { group, value, groupLabel, 2 more }`

  - `group: string`

    Raw group value (e.g. an ID). Use `groupLabel` for display.

  - `value: number`

  - `groupLabel: optional string`

  - `previousValue: optional number`

  - `segments: optional array of ReportSegmentDatum`

    - `segment: string`

    - `value: number`

    - `previousValue: optional number`

    - `segmentLabel: optional string`

### Report Metric

- `ReportMetric object { id, allowedAggregations, description, 13 more }`

  - `id: string`

    Metric ID — use as `metric` in query requests.

  - `allowedAggregations: array of "count" or "sum" or "avg" or 6 more`

    - `"count"`

    - `"sum"`

    - `"avg"`

    - `"median"`

    - `"min"`

    - `"max"`

    - `"range"`

    - `"percentile"`

    - `"value"`

  - `description: string`

  - `name: string`

  - `supportedFilterAttributeIds: array of string`

    Attribute IDs this specific metric accepts in filter expressions.

  - `supportedGroupByDimensions: array of string`

    Attribute IDs this specific metric accepts as `groupBy` or `segmentBy`.

  - `supportedViews: array of "standard" or "hourly_heatmap" or "sankey"`

    Which `view` values `POST /v2/reports/query` accepts for this metric. Most metrics support `standard` and `hourly_heatmap`; conversation-flow metrics are `sankey`-only.

    - `"standard"`

    - `"hourly_heatmap"`

    - `"sankey"`

  - `supportsOfficeHours: boolean`

  - `supportsPeriodComparison: boolean`

  - `type: "count" or "percentage" or "duration" or "number"`

    - `"count"`

    - `"percentage"`

    - `"duration"`

    - `"number"`

  - `unit: "count" or "percentage" or "duration_ms" or "number"`

    - `"count"`

    - `"percentage"`

    - `"duration_ms"`

    - `"number"`

  - `dataAvailableFromIso: optional string`

    Local calendar date (ISO 8601) from which this specific metric has data, when it differs from the global reporting floor. Windows ending before this date are rejected for this metric.

  - `isDefaultVariant: optional boolean`

    Whether this variant is the default selection for its `variantGroupId`.

  - `variantGroupId: optional string`

    Metrics sharing a `variantGroupId` are timestamp variants of one logical metric (same display `name`); each measures the same population at a different timestamp. Query the concrete metric `id` to pick a variant.

  - `variantId: optional string`

    Stable identifier for this variant within its group (e.g. `created_at`, `started_at`).

  - `variantLabel: optional string`

    Human-readable variant label for pickers (e.g. `Created at`, `Started at`).

### Report Segment Datum

- `ReportSegmentDatum object { segment, value, previousValue, segmentLabel }`

  - `segment: string`

  - `value: number`

  - `previousValue: optional number`

  - `segmentLabel: optional string`

### Report Time Series Datum

- `ReportTimeSeriesDatum object { date, value, previousValue, segments }`

  - `date: string`

    Time bucket start (ISO 8601).

  - `value: number`

  - `previousValue: optional number`

    Value for the same bucket in the comparison window, when requested.

  - `segments: optional array of ReportSegmentDatum`

    - `segment: string`

    - `value: number`

    - `previousValue: optional number`

    - `segmentLabel: optional string`

### Report List Datasets Response

- `ReportListDatasetsResponse object { data, object }`

  - `data: array of ReportDataset`

    - `id: string`

    - `attributes: array of ReportAttribute`

      - `id: string`

        Attribute ID — use as `fieldId` in filter rules and as `groupBy` / `segmentBy` in query requests.

      - `allowedOperators: array of "is" or "is_not" or "in" or 10 more`

        - `"is"`

        - `"is_not"`

        - `"in"`

        - `"not_in"`

        - `"contains"`

        - `"not_contains"`

        - `"gte"`

        - `"lte"`

        - `"between"`

        - `"exists"`

        - `"not_exists"`

        - `"is_member_of"`

        - `"is_not_member_of"`

      - `category: string`

      - `description: string`

      - `name: string`

      - `semantics: "current_state" or "historical_action" or "historical_snapshot" or "dynamic_metric_mapped"`

        Whether the filter uses the latest mutable state, an immutable historical action/snapshot, or a metric-dependent mapping.

        - `"current_state"`

        - `"historical_action"`

        - `"historical_snapshot"`

        - `"dynamic_metric_mapped"`

      - `supportsFilter: boolean`

        Whether the attribute can be used in `filters` rules.

      - `supportsGroupBy: boolean`

        Whether the attribute can be used as `groupBy` / `segmentBy`.

      - `supportsValueLookup: boolean`

        Whether `POST /v2/reports/filter-values` can list this attribute's values.

      - `valueType: "string" or "number" or "boolean" or 3 more`

        - `"string"`

        - `"number"`

        - `"boolean"`

        - `"date"`

        - `"enum"`

        - `"id"`

      - `multiValue: optional boolean`

      - `staticOptions: optional array of ReportAttributeOption`

        Fixed value set for enum-like attributes. Attributes without static options resolve values via `POST /v2/reports/filter-values`.

        - `label: string`

        - `value: string`

    - `description: string`

    - `metrics: array of ReportMetric`

      - `id: string`

        Metric ID — use as `metric` in query requests.

      - `allowedAggregations: array of "count" or "sum" or "avg" or 6 more`

        - `"count"`

        - `"sum"`

        - `"avg"`

        - `"median"`

        - `"min"`

        - `"max"`

        - `"range"`

        - `"percentile"`

        - `"value"`

      - `description: string`

      - `name: string`

      - `supportedFilterAttributeIds: array of string`

        Attribute IDs this specific metric accepts in filter expressions.

      - `supportedGroupByDimensions: array of string`

        Attribute IDs this specific metric accepts as `groupBy` or `segmentBy`.

      - `supportedViews: array of "standard" or "hourly_heatmap" or "sankey"`

        Which `view` values `POST /v2/reports/query` accepts for this metric. Most metrics support `standard` and `hourly_heatmap`; conversation-flow metrics are `sankey`-only.

        - `"standard"`

        - `"hourly_heatmap"`

        - `"sankey"`

      - `supportsOfficeHours: boolean`

      - `supportsPeriodComparison: boolean`

      - `type: "count" or "percentage" or "duration" or "number"`

        - `"count"`

        - `"percentage"`

        - `"duration"`

        - `"number"`

      - `unit: "count" or "percentage" or "duration_ms" or "number"`

        - `"count"`

        - `"percentage"`

        - `"duration_ms"`

        - `"number"`

      - `dataAvailableFromIso: optional string`

        Local calendar date (ISO 8601) from which this specific metric has data, when it differs from the global reporting floor. Windows ending before this date are rejected for this metric.

      - `isDefaultVariant: optional boolean`

        Whether this variant is the default selection for its `variantGroupId`.

      - `variantGroupId: optional string`

        Metrics sharing a `variantGroupId` are timestamp variants of one logical metric (same display `name`); each measures the same population at a different timestamp. Query the concrete metric `id` to pick a variant.

      - `variantId: optional string`

        Stable identifier for this variant within its group (e.g. `created_at`, `started_at`).

      - `variantLabel: optional string`

        Human-readable variant label for pickers (e.g. `Created at`, `Started at`).

    - `name: string`

  - `object: "list"`

    - `"list"`

### Report Query Response

- `ReportQueryResponse object { meta, object, value, 7 more }`

  - `meta: object { aggregation, datasetId, granularity, 5 more }`

    - `aggregation: "count" or "sum" or "avg" or 6 more`

      Aggregation function applied to the metric. Each metric supports a subset of aggregations — see the `allowedAggregations` field in the `GET /v2/reports/datasets` catalog.

      - `"count"`

      - `"sum"`

      - `"avg"`

      - `"median"`

      - `"min"`

      - `"max"`

      - `"range"`

      - `"percentile"`

      - `"value"`

    - `datasetId: string`

    - `granularity: string`

    - `metric: string`

    - `officeHoursOnly: boolean`

    - `unit: "count" or "percentage" or "duration_ms" or "number"`

      - `"count"`

      - `"percentage"`

      - `"duration_ms"`

      - `"number"`

    - `groupBy: optional string`

    - `segmentBy: optional string`

  - `object: "report_query_result"`

    - `"report_query_result"`

  - `value: number`

    Aggregated value across the whole reporting window.

  - `deltaPercent: optional number`

    Percentage change vs the comparison window.

  - `flowData: optional array of object { metricId, pathId, source, 7 more }`

    - `metricId: string`

    - `pathId: string`

    - `source: string`

    - `sourceLabel: string`

    - `target: string`

    - `targetLabel: string`

    - `value: number`

    - `colorKey: optional string`

    - `percentage: optional number`

    - `sortOrder: optional number`

  - `groupedData: optional array of ReportGroupedDatum`

    - `group: string`

      Raw group value (e.g. an ID). Use `groupLabel` for display.

    - `value: number`

    - `groupLabel: optional string`

    - `previousValue: optional number`

    - `segments: optional array of ReportSegmentDatum`

      - `segment: string`

      - `value: number`

      - `previousValue: optional number`

      - `segmentLabel: optional string`

  - `previousValue: optional number`

    Aggregated value for the comparison window, when requested.

  - `segmentData: optional array of ReportSegmentDatum`

    - `segment: string`

    - `value: number`

    - `previousValue: optional number`

    - `segmentLabel: optional string`

  - `table: optional object { columns, key, mode, 6 more }`

    - `columns: array of object { id, kind, label, 6 more }`

      - `id: string`

      - `kind: "dimension" or "metric" or "record_attribute"`

        - `"dimension"`

        - `"metric"`

        - `"record_attribute"`

      - `label: string`

      - `sortable: boolean`

      - `unit: "count" or "percentage" or "duration_ms" or 3 more`

        - `"count"`

        - `"percentage"`

        - `"duration_ms"`

        - `"number"`

        - `"text"`

        - `"datetime"`

      - `align: optional "left" or "right"`

        - `"left"`

        - `"right"`

      - `sticky: optional boolean`

      - `summary: optional "sum" or "weighted_rate" or "none"`

        - `"sum"`

        - `"weighted_rate"`

        - `"none"`

      - `valueType: optional string`

    - `key: string`

    - `mode: "aggregate" or "records"`

      - `"aggregate"`

      - `"records"`

    - `page: number`

    - `pageSize: number`

    - `rows: array of object { id, cells }`

      - `id: string`

      - `cells: map[object { display, denominator, numerator, 3 more } ]`

        - `display: string`

        - `denominator: optional number`

        - `numerator: optional number`

        - `raw: optional unknown`

        - `sortValue: optional string or number`

          - `string`

          - `number`

        - `value: optional unknown`

    - `totalRows: number`

    - `sort: optional object { columnId, direction }`

      - `columnId: string`

      - `direction: "asc" or "desc"`

        - `"asc"`

        - `"desc"`

    - `summaryRows: optional array of object { id, cells }`

      - `id: string`

      - `cells: map[object { display, denominator, numerator, 3 more } ]`

        - `display: string`

        - `denominator: optional number`

        - `numerator: optional number`

        - `raw: optional unknown`

        - `sortValue: optional string or number`

          - `string`

          - `number`

        - `value: optional unknown`

  - `timeSeries: optional array of ReportTimeSeriesDatum`

    - `date: string`

      Time bucket start (ISO 8601).

    - `value: number`

    - `previousValue: optional number`

      Value for the same bucket in the comparison window, when requested.

    - `segments: optional array of ReportSegmentDatum`

      - `segment: string`

      - `value: number`

      - `previousValue: optional number`

      - `segmentLabel: optional string`

### Report Lookup Filter Values Response

- `ReportLookupFilterValuesResponse object { data, object }`

  - `data: array of ReportAttributeOption`

    - `label: string`

    - `value: string`

  - `object: "list"`

    - `"list"`

### Report Drill In Response

- `ReportDrillInResponse object { availableColumns, defaultColumnIds, meta, 6 more }`

  - `availableColumns: array of object { id, name, supportsSort, 2 more }`

    - `id: string`

      Column ID (an attribute ID or `metric.value`).

    - `name: string`

    - `supportsSort: boolean`

      Whether this column may be used in the drill-in sort request.

    - `valueType: string`

    - `category: optional string`

  - `defaultColumnIds: array of string`

  - `meta: object { datasetId, metricId, rawMetricId, 6 more }`

    - `datasetId: string`

    - `metricId: string`

    - `rawMetricId: string`

    - `reconciliation: object { kind, rowCount, status, 4 more }`

      - `kind: "rows" or "actions_across_conversations" or "active_hour_rate" or 2 more`

        - `"rows"`

        - `"actions_across_conversations"`

        - `"active_hour_rate"`

        - `"positive_ratings_over_eligible_ratings"`

        - `"missed_over_evaluated"`

      - `rowCount: number`

      - `status: "not_applicable" or "available" or "denominator_unavailable"`

        - `"not_applicable"`

        - `"available"`

        - `"denominator_unavailable"`

      - `denominatorHours: optional number`

      - `displayedRate: optional number`

      - `eligibleDenominatorTotal: optional number`

      - `rawNumeratorTotal: optional number`

    - `rowGrain: string`

    - `rowSemantics: "metric_rows" or "numerator_rows"`

      - `"metric_rows"`

      - `"numerator_rows"`

    - `sourceFact: string`

    - `emptyReason: optional "no_matching_rows" or "denominator_unavailable"`

      - `"no_matching_rows"`

      - `"denominator_unavailable"`

    - `metricRowId: optional string`

  - `object: "report_drill_in_result"`

    - `"report_drill_in_result"`

  - `page: number`

  - `pageSize: number`

  - `rows: array of map[string or number or boolean or 3 more]`

    One record per underlying row, keyed by column ID.

    - `string`

    - `number`

    - `boolean`

    - `array of string`

    - `ReportDrillInIdentityCell object { id, kind, label, 3 more }`

      - `id: string`

      - `kind: "identity"`

        - `"identity"`

      - `label: string`

      - `avatarUrl: optional string`

      - `color: optional string`

      - `provenance: optional "current" or "historical" or "fallback" or "unassigned"`

        - `"current"`

        - `"historical"`

        - `"fallback"`

        - `"unassigned"`

    - `ReportDrillInSourceDetailsCell object { items, kind }`

      - `items: array of object { label, id, type, url }`

        - `label: string`

        - `id: optional string`

        - `type: optional string`

        - `url: optional string`

      - `kind: "source_details"`

        - `"source_details"`

  - `timezone: string`

  - `total: number`
