Skip to content
Dashboard

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.
  2. 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.
  3. 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.
  4. Optionally break results down with groupBy (primary dimension) and segmentBy (secondary dimension).
  5. 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

{
  "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 ParametersExpand Collapse
"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"
One of the following:
"2026-01-01.nova"
"2025-12-12.clover"
Body ParametersJSONExpand Collapse
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.

One of the following:
"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.

minLength10
maxLength64
granularity: "hour" or "day" or "week" or "month"

Time bucket size for the returned time series.

One of the following:
"hour"
"day"
"week"
"month"
metric: string

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

minLength1
maxLength128
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.

minLength10
maxLength64
compareEndDate: optional string

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

minLength10
maxLength64
compareStartDate: optional string

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

minLength10
maxLength64
filters: optional ReportFilterRule { fieldId, kind, operator, value } or ReportFilterExpression { children, kind, op }

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.

One of the following:
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.

minLength1
maxLength512
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.

One of the following:
"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.

One of the following:
string
number
boolean
array of string or number or boolean
One of the following:
string
number
boolean
ReportFilterExpression object { children, kind, op }
children: array of ReportFilterRule { fieldId, kind, operator, value } 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.

One of the following:
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.

minLength1
maxLength512
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.

One of the following:
"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.

One of the following:
string
number
boolean
array of string or number or boolean
One of the following:
string
number
boolean
ReportFilterGroup object { children, kind, op }
children: array of ReportFilterRule { fieldId, kind, operator, value }
fieldId: string

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

minLength1
maxLength512
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.

One of the following:
"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.

One of the following:
string
number
boolean
array of string or number or boolean
One of the following:
string
number
boolean
kind: "group"
op: "and" or "or"

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

One of the following:
"and"
"or"
kind: "group"
op: "and" or "or"

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

One of the following:
"and"
"or"
groupBy: optional string

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

maxLength512
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.

minimum1
maximum100
segmentBy: optional string

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

maxLength512
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.

One of the following:
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.

maxLength64
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.

One of the following:
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.

One of the following:
"standard"
"hourly_heatmap"
"sankey"
ReturnsExpand Collapse
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.

One of the following:
"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"
One of the following:
"count"
"percentage"
"duration_ms"
"number"
groupBy: optional string
segmentBy: optional string
object: "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, 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, value, previousValue, segmentLabel }
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, value, previousValue, segmentLabel }
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"
One of the following:
"dimension"
"metric"
"record_attribute"
label: string
sortable: boolean
unit: "count" or "percentage" or "duration_ms" or 3 more
One of the following:
"count"
"percentage"
"duration_ms"
"number"
"text"
"datetime"
align: optional "left" or "right"
One of the following:
"left"
"right"
sticky: optional boolean
summary: optional "sum" or "weighted_rate" or "none"
One of the following:
"sum"
"weighted_rate"
"none"
valueType: optional string
key: string
mode: "aggregate" or "records"
One of the following:
"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
One of the following:
string
number
value: optional unknown
totalRows: number
sort: optional object { columnId, direction }
columnId: string
direction: "asc" or "desc"
One of the following:
"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
One of the following:
string
number
value: optional unknown
timeSeries: optional array of ReportTimeSeriesDatum { 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, value, previousValue, segmentLabel }
segment: string
value: number
previousValue: optional number
segmentLabel: optional string

Run a report query

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"
        }'
{
  "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"
        }
      ]
    }
  ]
}
Returns Examples
{
  "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"
        }
      ]
    }
  ]
}