---
title: "Add a chart to a dashboard"
method: POST
path: "/api/v3/dashboards/{dashboardId}/widgets"
tags: ["V3 Dashboards"]
---

# Add a chart to a dashboard

`POST /api/v3/dashboards/{dashboardId}/widgets`

## Request

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

Puts an existing chart on a dashboard. Without a `layout` the chart lands in the first open slot
at its chart type's default size; with one, exactly there. Widgets may leave gaps but not overlap:
a layout covering another widget answers 422 naming it. A chart appears at most once per
dashboard (409), and must live in the dashboard's workspace. A dashboard holds at most 250
widgets, and a new widget takes the first open slot: an add past the cap, or with no open slot
above the grid's last row, answers 409. Moves the dashboard's `updatedAt`.
Needs write access to the dashboard's workspace.

## Path parameters

- `dashboardId` string, cuid2, required

## Request body

- WidgetCreateRequest
  - `chartId` string, cuid2, required: A chart in the dashboard's workspace. A chart appears at most once per dashboard.
  - `layout` WidgetLayout: A widget's position and size on the dashboard's 12-column grid, in whole grid units: `1 ≤ w ≤ 12`, `x + w ≤ 12`, `1 ≤ h ≤ 8`, `0 ≤ y ≤ 1000`. Widgets may not overlap; gaps are allowed and close upward when the dashboard is drawn.
    - `x` integer, required
    - `y` integer, required
    - `w` integer, required
    - `h` integer, required

## Response `201`

The new widget

- object
  - `data` DashboardWidget, required: One chart's placement on the dashboard. `chart` is exactly what reading the chart returns.
    - `id` string, cuid2, required
    - `chartId` string, cuid2, required
    - `layout` WidgetLayout, required: A widget's position and size on the dashboard's 12-column grid, in whole grid units: `1 ≤ w ≤ 12`, `x + w ≤ 12`, `1 ≤ h ≤ 8`, `0 ≤ y ≤ 1000`. Widgets may not overlap; gaps are allowed and close upward when the dashboard is drawn.
      - `x` integer, required
      - `y` integer, required
      - `w` integer, required
      - `h` integer, required
    - `order` integer, required
    - `chart` ChartResource, required: A chart: one saved visualisation of one Feedback Dataset. It belongs to a workspace and can sit on any number of dashboards — `dashboards` lists them, so a caller can see what an edit or a delete would affect.
      - `id` string, cuid2, required
      - `workspaceId` string, cuid2, required
      - `datasetId` string, cuid2, required: The Feedback Dataset the chart reads. Fixed when the chart is created.
      - `name` string, required: Unique within the workspace.
      - `type` 'area' | 'bar' | 'pie' | 'big_number' | 'matrix', required
      - `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
              - …
            - object
              - …
            - object
              - …
        - `order` union: Sort order: `[field, direction]` pairs, or an object mapping a field to its direction. At most 10 entries.
          - array[]
            - unknown[]
              - …
          - object
        - `limit` integer
        - `offset` integer
      - `config` ChartConfig, required: Display settings, kept apart from the chart query. Every key is optional; each applies to the chart types named in its description and is ignored by the others.
        - `barOrientation` 'vertical' | 'horizontal': Bar charts. Defaults to `vertical`.
        - `pieDisplay` 'pie' | 'breakdown': Pie charts. Defaults to `pie`.
        - `areaDisplay` 'filled' | 'line': Area charts. Defaults to `filled`.
        - `matrixCellValue` 'percent' | 'count' | 'both': Matrix charts. Defaults to `percent`.
        - `matrixColorScale` boolean: Matrix charts. Defaults to `true`.
        - `matrixShowTotals` boolean: Matrix charts. Defaults to `false`.
        - `matrixTranspose` boolean: Matrix charts. Swaps rows and columns on screen; the query is unchanged.
        - `colors` string[]
        - `xAxisLabel` string
        - `yAxisLabel` string
        - `showLegend` boolean
        - `legendPosition` 'top' | 'bottom' | 'left' | 'right'
        - `stacked` boolean
        - `showGrid` boolean
        - `showValues` boolean
        - `numberFormat` string
        - `dateFormat` string
        - `prefix` string
        - `suffix` string
      - `creator` object, nullable, required: Who created the chart, for display only; `null` once that user is gone.
        - `name` string, required
      - `dashboards` object[], required: The dashboards that hold the chart, by name.
        - `id` string, cuid2, required
        - `name` string, required
      - `createdAt` string, date-time, required
      - `updatedAt` string, date-time, required

## 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.
- `409`: Conflict (`code: conflict`) — the request clashes with the current state: a chart or dashboard name already used in the workspace, a chart already on the dashboard, a dashboard already holding its 250 widgets, a dashboard with no open slot left above its grid's last row, or a dashboard changed since the `expectedUpdatedAt` the caller sent.
- `422`: Unprocessable Content — semantic validation failed or the requested lifecycle transition is invalid.
- `429`: Rate limit exceeded.
- `500`: Internal Server Error.

## Changes

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

[Operation history](https://skmtc.dev/formbricks/apis/formbricks-api-v3/changes/api/v3/dashboards/:dashboardId/widgets/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)
