---
title: "Save a whole dashboard"
method: PUT
path: "/api/v3/dashboards/{dashboardId}"
tags: ["V3 Dashboards"]
---

# Save a whole dashboard

`PUT /api/v3/dashboards/{dashboardId}`

## Request

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

Replaces a dashboard's name and every widget's place in one transaction — all of it or none of
it. A widget left out is removed; its chart is kept. Widgets may leave gaps but not overlap (422).
Send the `updatedAt` from your read as `expectedUpdatedAt`: if the dashboard changed since, the save
is refused with 409 and nothing is written. Needs write access to the dashboard's workspace.

## Path parameters

- `dashboardId` string, cuid2, required

## Request body

- DashboardReplaceRequest: The whole edited dashboard: its name and every widget's place. A widget left out of `widgets` is removed (its chart is kept). `expectedUpdatedAt` is the dashboard's `updatedAt` from your read; if it changed since, nothing is saved.
  - `name` string, required
  - `expectedUpdatedAt` string, date-time, required: The dashboard's `updatedAt` from your read.
  - `widgets` object[], required
    - `widgetId` 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: Sort key; any non-negative integer, in the order widgets should list.

## Response `200`

The saved dashboard

- object
  - `data` DashboardResource, required: A dashboard: a named grid of widgets in a workspace. Its charts can come from different Feedback Datasets. Each widget's data is read separately, from `GET /api/v3/charts/{chartId}/data`.
    - `id` string, cuid2, required
    - `workspaceId` string, cuid2, required
    - `name` string, required: Unique within the workspace.
    - `creator` DashboardCreator, nullable, required: Who created the dashboard, for display only; `null` once that user is gone.
      - `name` string, required
    - `widgets` DashboardWidget[], required: In display order.
      - `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.
              - …
          - `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.
              - …
          - `order` union: Sort order: `[field, direction]` pairs, or an object mapping a field to its direction. At most 10 entries.
            - array[]
              - …
            - 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
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required: Changes on every rename, save and widget change. Send it back on a full update to be told if the dashboard changed in between.

## 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 — the dashboard changed since your read, or the name is already used in the workspace. Nothing was written. For a changed dashboard, `details` carries `expectedUpdatedAt` and the live `currentUpdatedAt`: read it again and retry.
- `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/put.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)
