---
title: "Build a dashboard from a description"
method: POST
path: "/api/v1/rest/bi/dashboards"
tags: ["Business Intelligence"]
---

# Build a dashboard from a description

`POST /api/v1/rest/bi/dashboards`

## Request

- Base URL: `https://api.custodo.ai`
- URL: `https://api.custodo.ai/api/v1/rest/bi/dashboards`
- Auth: HTTP bearer

Builds a NEW dashboard from what the person described — its name and its charts (widgets) — and saves it PRIVATE to them: nobody else sees it, and no tool can share it (they do that themselves, from the board in the app). Each widget measures one dataset grouped by one of its dimensions, so discover them first: custodo_query_analytics with no arguments lists every dataset you may chart, with its dimensions, splits and filters — copy those keys exactly. Chart shapes follow the axis: grouped by time line/area/column/bar/table/stat, by anything else column/bar/donut/table/stat, a one-number dataset stat/column/bar/table; omit viz for the first. A dataset whose `windowed` is false measures the present and takes no range and no compare. At most 24 widgets, and 20 boards per person — the organisation’s plan may allow fewer. Whatever the catalogue does not offer you is refused with the valid choices, and nothing is saved. Returns the board id and `open_in_web`: give the person the link, and read the numbers with bi_run_dashboard.

## Headers

- `X-Tenant-Id` string, uuid

## Request body

- object
  - `name` string, required: The board’s name, in the person’s words.
  - `widgets` object[], required: The charts, in reading order: at most 24.
    - `dataset` string, required: A dataset key exactly as custodo_query_analytics lists it.
    - `title` string: The card’s caption. Omit to show the dataset’s own name, in the reader’s language.
    - `viz` 'stat' | 'bar' | 'column' | 'line' | 'area' | 'donut' | 'table': The chart shape. Omit for the axis’ default.
    - `dimension` string: One of the dataset’s dimensions to group by. Omit for its first.
    - `split_by` string, nullable: One of the dataset’s splits: a second axis that breaks every bucket down.
    - `grain` 'day' | 'week' | 'month' | 'quarter' | 'year', nullable: Bucket width on a time axis. Omit to fit the window.
    - `range` 'last_7_days' | 'last_30_days' | 'last_90_days' | 'last_12_months' | 'this_month' | 'this_year' | 'all_time' | 'custom': The window measured. Omit for the organisation’s default.
    - `from` string, nullable: With range "custom" only.
    - `to` string, nullable: With range "custom" only.
    - `filters` object: Filters the dataset declares, by key. A data filter takes an id from its own module — a document type from documents_list_types.
    - `sort` 'default' | 'value_desc' | 'value_asc' | 'label_asc' | 'label_desc'
    - `compare` 'none' | 'previous_period' | 'previous_year': Also measure the previous period or year, as a delta.
    - `row_limit` integer: Bars, slices or rows to draw, 1–200. Default 12.
    - `width` integer: Columns of 12: 3, 4, 6 (default) or 12.
  - `range` 'last_7_days' | 'last_30_days' | 'last_90_days' | 'last_12_months' | 'this_month' | 'this_year' | 'all_time' | 'custom': One window for the whole board, over each widget’s own. Omit to leave each its own.
  - `from` string, nullable: With range "custom" only.
  - `to` string, nullable: With range "custom" only.
  - `filters` object: Board-wide filters: each narrows every widget whose dataset declares that key.

## Response `200`

Done. `data` is the tool’s own answer.

- RestResult
  - `ok` true, required
  - `data` unknown, required

## Other responses

- `400`: The arguments did not validate: `error.message` names the field, what it expects and what arrived.
- `401`: No API key, or one that is malformed, unknown, revoked or expired.
- `403`: The call never ran: the module is off for this organisation, the credential lacks the permission or the module, the tier does not cover it, or the tool is destructive and the key was not minted for that.
- `409`: The record is in a state that does not allow it — under legal hold, already sent, already closed. `error.code` says which.
- `422`: The tool ran and refused, in a sentence of its own (`TOOL_REFUSED`): the message says what to change. Retrying the same call will not help.
- `429`: The API key has spent its minute, or its day. Every request a key makes counts. See `Retry-After`.

---

[API](https://skmtc.dev/custodo/apis/custodo-integrazioni.md) · [All operations](https://skmtc.dev/custodo/apis/custodo-integrazioni/llms.txt) · [OpenAPI document](https://skmtc.dev/custodo/apis/custodo-integrazioni/revisions/16d336d3ec2a?raw)
