---
title: "Preview a chart"
method: POST
path: "/api/v3/charts/preview"
tags: ["V3 Charts"]
---

# Preview a chart

`POST /api/v3/charts/preview`

## Request

- Base URL: `https://app.formbricks.com`
- URL: `https://app.formbricks.com/api/v3/charts/preview`
- Auth: one of:
  - API key in cookie `next-auth.session-token`
  - API key in header `x-api-key`
  - HTTP bearer

Runs an unsaved chart and returns its rows, so a caller can check it shows what was meant before
saving it. Saves nothing. The chart query is checked by the chart query validator first: a field
outside the catalog, a refused Cube option, a `timezone`, a bound exceeded or an invalid date
range answers 422 naming each problem in `invalid_params`. On a chart grouped by time
alone, a date range wider than 10,000 buckets of its granularity is one such bound
(`limit_exceeded`).

Needs read access to the workspace and the Feedback Dataset. Counts against the chart-query
budget (600 per minute, 20 at once, per user or API key), not the v3 default.

## Request body

- ChartPreviewRequest
  - `workspaceId` string, cuid2, required
  - `datasetId` string, cuid2, required: The Feedback Dataset to read; needs read access to it from `workspaceId`.
  - `type` 'area' | 'bar' | 'pie' | 'big_number' | 'matrix', required: The chart type. A `big_number` shows one value, so its grouping is dropped.
  - `query` ChartQuery, required: What a chart computes, in Cube's query format (ADR 0001): measures, dimensions, time dimensions, filters, order and limit over its Feedback Dataset's fields. Every field must come from the field catalog (`GET /api/v3/charts/fields`), named with its data-source prefix. Display settings are separate (`config`). Refused with **422** (`invalid_params` names each problem): a field that is not in the catalog; Cube options the app does not use (`ungrouped`, `segments`, `renewQuery`, `joinHints`, `total`, `compareDateRange`); a `timezone` (every range is read in the organization's display time zone); more than 15 fields across `measures`, `dimensions` and `timeDimensions`; more than 25 filter conditions or filters nested deeper than three levels; a date range that is not a preset, `all-time` or two ISO calendar dates; on a chart grouped by time alone, a date range that spans more than 10,000 buckets of its `granularity` (code `limit_exceeded`).
    - `measures` string[]
    - `dimensions` string[]
    - `timeDimensions` object[]
      - `dimension` string, required: A field of kind `timeDimension`.
      - `granularity` 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
      - `dateRange` union: A date range on a time dimension: a named preset from the field catalog's `datePresets` (`last 7 days`, `this month`, …), `all-time` (no range), or a pair of ISO calendar dates `[from, to]`, both inclusive, `from` not after `to`. Every range is read in the organization's display time zone; a `timezone` cannot be supplied. Charts saved before ranges were limited to presets may hold a relative range Cube reads itself (`last 3 months`, `this week`). Such a chart still renders and can be previewed, within the bucket limit below, and can be saved with that same range kept; a new chart, or an update to another range, must use a preset or a pair. A chart grouped by time alone (one time dimension with a `granularity`, no other dimensions) gets one row per bucket in its range, empty or not, so its range may span at most 10,000 buckets: about 13 months by the hour, or 27 years by the day. A wider range is refused with **422** `limit_exceeded`; use a coarser granularity or a shorter range. An older chart's relative range is measured back from now (`last 999 years` spans 999 years). Every preset fits at every granularity, and a chart with other dimensions returns only its real rows, so it is not limited.
        - string: A preset name from `datePresets`, or `all-time`.
        - string[]
    - `filters` ChartQueryFilter[]
      - union: A filter: either one condition on a field, or an `and` / `or` group of filters. Filters nest at most three levels: a group, a group inside it, and conditions inside that. A chart query holds at most 25 conditions in all.
        - object
          - `member` string, required: A field name from the field catalog.
          - `operator` 'equals' | 'notEquals' | 'contains' | 'notContains' | 'set' | 'notSet' | 'gt' | 'gte' | 'lt' | 'lte', required: One of the field's `filterOperators` in the catalog.
          - `values` string[]: The values to compare against. Required by every operator except `set` and `notSet`, which take none.
        - object
          - `and` ChartQueryFilter[], required
        - object
          - `or` ChartQueryFilter[], required
    - `order` union: Sort order: `[field, direction]` pairs, or an object mapping a field to its direction. At most 10 entries.
      - array[]
        - unknown[]
          - unknown
      - object
    - `limit` integer
    - `offset` integer
  - `dateRange` union: Replaces the query's date range on `FeedbackRecords.collectedAt`: a preset, `all-time` to remove it, or `{ from, to }` ISO calendar dates. Read in the organization's display time zone.
    - string
    - object
      - `from` string, date, required
      - `to` string, date, required

## Response `200`

The chart's rows

- object
  - `data` ChartData, required: What a chart query returns: one row per group, keyed by field name, the label maps to display stored ids with, and the query as it ran.
    - `rows` object[], required
    - `optionLabels` object, required: Choice option ids to their labels in the survey's default language, in survey order. Filled when the rows group by `FeedbackRecords.valueId` (or a `valueText` grouping the server rewrote to it); empty otherwise.
    - `fieldLabels` object, required: Matrix row field ids (`<elementId>__<rowId>`) to their statements, in survey order. Filled when the rows group by `FeedbackRecords.fieldId` over matrix questions; empty otherwise.
    - `query` object, required: The query as it ran: after the requested date range replaced the saved one, and after a `valueText` grouping on a single-choice question was rewritten to `valueId`. The row keys follow this query.

## Other responses

- `400`: Bad Request — malformed JSON, invalid query/body/params, duplicate name, or unsupported field.
- `401`: Not authenticated (no valid session or API key).
- `403`: Forbidden. `code: forbidden` when the caller has no access to the workspace, the Feedback Dataset or the resource (which may also not exist; 404 is not used, to avoid leaking existence). `code: dashboards_plan_required` when the caller is authorized but the organization does not have the Dashboards add-on or a license that includes it — checked after authorization, so a caller without access never learns the organization's plan.
- `422`: Unprocessable Content — semantic validation failed or the requested lifecycle transition is invalid.
- `429`: Too Many Requests (`code: too_many_requests`). Chart data, preview and field-value lookups share one budget of 600 requests per minute per user or API key, separate from the 100 per minute of the rest of v3, so a dashboard can load every widget's data. At most 20 of them run at once per user or key; past that the answer is 429 with `Retry-After: 1`. `Retry-After` says when to retry.
- `500`: Internal Server Error.
- `502`: Bad Gateway (`code: bad_gateway`) — the chart query could not be run: the analytics engine was unreachable, refused it, or it ran longer than the one-minute limit and was stopped. The engine's own error is never returned. Retrying later is safe; a query that timed out needs a narrower date range or grouping.

## Changes

- **2026-10-28** `84820a01e550`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/formbricks/apis/formbricks-api-v3/changes/api/v3/charts/preview/post.md)

---

[API](https://skmtc.dev/formbricks/apis/formbricks-api-v3.md) - [All operations](https://skmtc.dev/formbricks/apis/formbricks-api-v3/llms.txt) - [OpenAPI document](https://skmtc.dev/formbricks/apis/formbricks-api-v3/revisions/25335c959a13?raw)
