---
title: "Rewrite the definition of a widget"
method: POST
path: "/widgets/update"
tags: ["Widgets"]
---

# Rewrite the definition of a widget

`POST /widgets/update`

## Request

- Base URL: `https://api.reportingninja.com/v1`
- URL: `https://api.reportingninja.com/v1/widgets/update`
- Auth: HTTP bearer

Replaces what a widget shows with the definition in dsl. The whole definition travels, not the part of it that changes: the widget is rebuilt from it, exactly as the assistant of the application rebuilds one. Read it with /v1/widgets/get, change what you want and send it back.

What the rewrite KEEPS of the widget: the connection it reads through, the row and the column it is in, its format property by property, the comparison with the previous period of a KPI or a metrics summary, the literals of a custom period header, the breakdown by source and the deal pipeline of a HubSpot widget, the title of every column of a metrics summary whose metric is still there, and the internal notes unless internal_notes is sent. Two properties are kept only while the shape does not change, because the shape is what chooses them: the legend position (a pie hides it) and the headers of the periods (a metrics summary dates them). What it cannot keep comes back in lost_properties. /v1/capabilities publishes the whole list under widgets.update.

What it does NOT read: the integration of a widget cannot be changed, so there is no integration_id here; full_width is ignored, because the place of a widget belongs to the report and an update never moves anything.

Every call keeps the widget as it was first and answers with the backup_key that puts it back through /v1/widgets/restore, for 7 days. There is no confirmation step for that reason.

Refusals: 400 for the framework and the body, 400 APPEND_NOT_ALLOWED with reason template_based for a report built on a template -- where only the text widget the report keeps of its own can be changed -- 403 WRITE_ACCESS_DISABLED, 404 NOT_FOUND, 409 WIDGET_NOT_EDITABLE when the widget carries something the DSL cannot describe, 409 WIDGET_CHANGED when if_widget_hash is stale or when the widget changed after the call read it (nothing is written and no backup is kept), 409 DATA_VIEW_CHANGED, 409 ROW_RULE when the new shape does not fit the row the widget is in, and 409 REPORT_IN_TRASH.

Every write endpoint reads dry_run and idempotency_key; see the Write guarantees section of the introduction.

What a widget COMPARES is decided when it is created: this call does not change the class of a comparator, does not turn an ordinary widget into one and does not stop one being one, and a body that tries is refused with 400 VALIDATION_FAILED and the problem COMPARATOR_CLASS_FIXED. Whether it is broken down over time is part of that class: `over_time` moves from one axis to another, never from none to an axis or back. A body with no `compare` at all leaves the comparator exactly as it is -- the compared elements, the axis, the second series and the shape it is drawn as are kept -- and a body that carries one replaces them whole. A body with no `compare` keeps the order and the cap of the comparator too, so on a comparator not drawn as a trend a `sort_by` or a `limit` is refused with 400 VALIDATION_FAILED and the problem COMPARE_KEY_NOT_FOR_THIS_COMPARATOR (a dry run answers valid false with the same problem). The one cap a body with no `compare` does write is the one of a breakdown: its `limit` is the number of series it is broken down into, and a `limit` of null keeps the one it has. In a report whose report_level_data_sources is true the accounts of a comparator are the report's: send compare.items exactly as the read published them, every field of every item and in the same order, and a body that changes anything in them answers 409 NOT_ALLOWED with details.reason report_level_data_sources; nothing an item carries is accepted and dropped.

## Request body

- WidgetsUpdateRequest: Which widget to rewrite and what it should show, or what its AI summary should read.
  - `report_key` string, required: The report the widget belongs to.
  - `section_key` string, required: The section the widget is in.
  - `widget_key` string, required: The widget to rewrite.
  - `dsl` WidgetDsl
    - `name` string, required: Title of the widget, in the requested output language. Plain text, no markup. At most 80 characters.
    - `visualization` 'kpi' | 'graph_basic' | 'graph_pie' | 'graph_trend' | 'table_metrics_summary' | 'table_dimensions_metrics' | 'text', required: How the widget is drawn. The first six read data; text is a block of prose with no data behind it, and it is the only one that carries content.
    - `chart_type` 'area' | 'line' | 'column' | 'bar', nullable, required: Which chart a graph_basic (area, line, column or bar) or a graph_trend (area, line or column; a trend draws no bars) is drawn as. Null takes the default of the shape, which is a column chart for a graph_basic and an area chart for a graph_trend, and is the usual answer. A chart drawn along a single time dimension is built as a trend and takes the charts of a trend, so it never carries bar. The four shapes that draw no chart always carry null.
    - `full_width` boolean, nullable, required: True when the widget takes a row of its own; null, like false, pairs it up. The layout of a section is yours: the widgets you do not mark are laid out two to a row in the order you write them, and the KPIs go four to a row before them. A widget marked true closes the row being filled and the next widget opens a new one. Ignored in a kpi, which is always part of a strip of four.
    - `content` string, nullable, required: The prose of a text block, and null in every other shape. Markdown of a reduced set and nothing else: blank line between paragraphs, **bold**, lines starting with '- ' for a bullet list and with '1. ' for a numbered one. No headings, no links, no italics, no images, no HTML and no emoji; anything else is printed as the characters it is made of. At most 2000 characters. A text block carries no figures: it is written before the widgets of the report are run, so any number in it would be one nobody measured.
    - `data_view` string, required: Public id of the data view the widget reads, exactly as listed in the catalogue. Empty string when the integration has no data views, and empty string in a text block, which reads none.
    - `metrics` union[], required: Public field ids of the metrics, in the order they are shown. At least one in the six shapes that read data; empty in a text block. A column of a table_metrics_summary may also be written as the object {id, filter_key}, which reads that one column through a saved filter of the account. Set by API and MCP clients; a definition being generated always writes the field id on its own.
      - union
        - string
        - object
          - `id` string, required: Public field id of the metric, the same one the short form carries.
          - `filter_key` string, nullable, required: Key of a saved filter of the account this column is read through, or null when the column is read like every other one. Only a table_metrics_summary takes it, and only on integrations whose columns carry a filter of their own. Set by API and MCP clients; a definition being generated always writes it as null.
    - `dimensions` string[], required: Public field ids of the dimensions, in the order they are shown. Empty when the widget has none.
    - `filter_key` string, nullable, required: Key of a saved filter of the account the whole widget is read through, or null when the widget reads no such filter. It excludes filters: a widget either points at a filter of the account or carries conditions of its own, never both. The filter has to belong to the integration of the widget and, where the filters of that integration are read in a data view, to the data view of the widget. Set by API and MCP clients; a definition being generated always writes it as null.
    - `compare` object, nullable, required: What the widget is drawn several times over, or null when it compares nothing, which is the usual answer. accounts draws one series per connected account and takes at least two of them; filters draws one per saved filter of the account; breakdown splits one page metric of facebook_insights into a series per value -- the metric itself says what it is broken down by -- and only a graph_trend takes it. A comparator is only written when the widget is created: neither the class of comparator, nor whether it is broken down over time, nor the fact of being one is changed later. Which classes an integration offers depends on the entity level of its data view too. Set by API and MCP clients; a definition being generated always writes it as null.
      - `by` 'accounts' | 'filters' | 'breakdown', required: What the widget compares.
      - `items` union, required: What is compared, in the order it is drawn: one object per connected account for accounts, one per saved filter for filters, and null for a breakdown, which compares no list.
        - object[]
          - `connection_key` string, required: Connection the account is read through, as the list of connections publishes it.
          - `account_id` string, required: Account of that connection, as the list of connections publishes it.
          - `label` string, nullable, required: Title this series is drawn under, or null for the one the platform builds. It cannot contain the sequence #%#, which is what the parts of a stored account are joined with.
          - `filter_key` string, nullable, required: Key of a saved filter of the account this one account is read through, or null when it is read whole. The same account is never compared twice.
        - object[]
          - `filter_key` string, required: Key of a saved filter of the account, as the list of filters publishes it. The same filter is never compared twice.
      - `over_time` 'none' | 'hour_of_day' | 'day_of_week' | 'date' | 'week' | 'month' | 'year', nullable, required: The axis a table_metrics_summary comparator is broken down along, which is what makes it an over time table. Null, like none, draws one column per compared element and is the usual answer. Only a table_metrics_summary takes it, and which axes a source offers depends on the integration and on the entity level of its data view. Switching the axis on or off is a change of subtype and is not done by an update.
      - `show_past_serie` boolean, nullable, required: True when a breakdown draws the period before as a second series. Null, like false, draws only the current period, and it is null in the other two comparators.
    - `filters` object, nullable, required: Filter applied to the widget, or null for no filter.
      - `combination` 'and' | 'or', required: How the conditions are joined. Only a combination the catalogue declares for this integration.
      - `conditions` object[], required
        - `field` string, required: Public id of a filter field of the catalogue.
        - `operator` string, required: Operator declared by the catalogue for that field or category.
        - `value` string, required: Value to compare against, always as text: digits for numbers, comma separated for list operators.
        - `type_of_field` string, nullable, required: dimension or metric, only when the catalogue says this integration requires it; null otherwise.
    - `sort_by` object, nullable, required: Ordering of the rows, or null to leave the default.
      - `field` string, required: Public field id, present in metrics or dimensions.
      - `direction` 'asc' | 'desc', required
    - `limit` integer, nullable, required: Maximum number of rows, or null for the default.
    - `date_range` object, nullable, required: Date range of the widget, or null to inherit the range of the report section.
      - `preset` string, required: One of the preset ids listed in the instructions, or custom when start and end are given.
      - `x` integer, nullable, required: Number of days, weeks or months of a preset that takes one; null otherwise.
      - `start` string, nullable, required: First day of a custom range as yyyy-MM-dd; null otherwise.
      - `end` string, nullable, required: Last day of a custom range as yyyy-MM-dd; null otherwise.
    - `previous_period` boolean, nullable, required: The comparison with the period immediately before, and each shape draws it its own way: a graph_trend draws it as an extra series, a table_dimensions_metrics as a column with the percentage of change of each metric, and a kpi and a table_metrics_summary as the figure of that period beside the current one. A few sources and views do not offer it and say so in a warning. In the two shapes that draw the figure, null means yes, which is what those widgets show by default; in the other two, null is the usual answer and asks for nothing.
    - `show_change` boolean, nullable, required: The PERCENTAGE OF CHANGE against the period before, drawn only by a kpi and by a table_metrics_summary, where it is the other half of the comparison: previous_period is the figure of that period and this one is the percentage beside it. The two are independent and neither governs the other. Null means yes, which is what those widgets show by default. The other five shapes carry null: the change asked for in one of them is ignored with a warning.
    - `settings` object[], nullable, required: Integration settings the catalogue declares, or null when none applies.
      - `id` string, required: Setting id of the catalogue.
      - `value` string, required: One of the values the catalogue lists for that setting.
    - `unfulfilled` string[], required: What was asked for and cannot be expressed with this integration, one short sentence each. Empty when everything asked for is in the widget.
  - `ai_summary` WidgetAiSummary: An AI summary as the body of a write says it, and as a read publishes it: the reach of the summary, what that reach reads, the settings it is generated with and the brief of this summary. The settings and the brief are a partial write: what a body does not name is left exactly as it is, a setting sent with the value the summary already uses is not written either, and a creation that names none of them is generated with the defaults /v1/capabilities publishes. It is not a visualization of the DSL, and it could not be: what a summary is made of is a selection of widgets or of sections, which is said with keys of this account's report, while a definition is portable and knows no keys. A TEMPLATE takes none: what a template holds is copied into every report built on it, and a summary reads the widgets and the sections of ONE report by key, so each copy would be a summary of nothing; it is refused with APPEND_NOT_ALLOWED and reason template_report.
    - `scope` 'widget' | 'section' | 'report', required: What the summary reads. `widget` reads the widgets of its own section it names, `section` reads the data widgets of its own section and `report` reads the sections it names. The scope of a summary that exists is never changed: a summary of another scope is placed and counted differently, so it is deleted and written again.
    - `widget_keys` string[]: Scopes `widget` and `section`: the data widgets of THIS section the summary reads. THREE STATES. Left out (or null) does not touch it: a creation falls back to what the editor preselects -- the first data widgets of the section -- and an update leaves the summary reading exactly what it reads. An EMPTY list is a value and means the summary reads nothing, which is the state the application saves a summary in with nothing ticked: it is accepted and the answer carries a SELECTION_EMPTY warning, because it generates nothing until something is selected. A list of keys replaces the selection. It is required on a CREATION of scope `widget`, which is made of them, and not on an update of one. /v1/capabilities publishes how many each scope may read.
    - `section_keys` string[]: Scope `report`: the sections of this report the summary reads. The same three states as widget_keys: left out (or null) does not touch it -- a creation reads the first sections of the report, an update keeps what the summary has -- an EMPTY list means it reads nothing and answers a SELECTION_EMPTY warning, and a list of keys replaces the selection.
    - `position` 'first' | 'last': Scopes `section` and `report`, and only when the summary is being CREATED: where it goes inside the section, alone in its row. `first`, the default, puts it above everything the section carries and moves the rest down; `last` opens a new row after the last one. A summary of scope `widget` is one more widget of its row and takes `placement` instead.
    - `ai_prompt` string, nullable: The instruction this summary is written with. A read answers the text or null, and never an empty string: an emptied box is stored as nothing, so a text that was emptied and one nobody ever wrote come back the same way. In a body, leaving the property out or sending null keeps what the summary has, an empty string empties it and a text replaces it.
    - `ai_include_comparison` boolean: Whether the summary is given the period before to compare against. True unless it was turned off.
    - `ai_auto_refresh` boolean: Whether the summary is written again on its own when the period or the data behind it changes. True unless it was turned off.
    - `ai_fallback_policy` 'omit' | 'default_text' | 'last_cached': What the end client is shown when the summary could not be produced: `omit` prints nothing, `default_text` prints ai_fallback_text and `last_cached` prints the last summary that worked, labelled with the period it belongs to. `omit` unless it was changed.
    - `ai_quota_fallback_policy` 'omit' | 'default_text' | 'last_cached': The same choice for the case where the monthly AI allowance of the account is used up, which is a different question: the data is healthy there. `omit` unless it was changed.
    - `ai_fallback_text` string, nullable: The text both policies print when they are set to `default_text`. It is never required: a policy of `default_text` with no text prints nothing, exactly as the settings window of the application saves it. A read answers the text or null and never an empty string; in a body, null keeps what the summary has and an empty string empties it.
    - `ai_show_disclaimer` boolean: Whether the summary carries the line saying it was written by AI. True unless it was turned off.
    - `ai_brief` string, nullable: What the summaries of THIS widget should know about the client. It is the narrowest of three levels and they are added together, not replaced: the level of the account is written in the application, the level of the report travels on /v1/reports/update, and this one belongs to the summary. Three states: leaving the property out -- or sending null, which is what a read answers when there is none -- keeps the brief the summary has, an empty string removes it, and a text replaces it. It is stored apart from the widget: it is not covered by widget_hash, /v1/widgets/restore does not bring it back, and a brief that could not be written comes back as an AI_BRIEF_NOT_SAVED warning of a 200.
    - `credits_per_generation` integer: What ONE generation of this summary costs the account, in credits. It is spent when the report is opened, viewed or delivered by a schedule, and never when the summary is written.
    - `credits_note` string: When those credits are spent.
  - `internal_notes` string: The notes of the widget, which nothing else shows to a client. Leave the property out to keep what is stored; send an empty string to empty them. Null is refused.
  - `allow_data_view_change` boolean: Accept a definition that reads a different data view than the widget does. Without it that call is HTTP 409 DATA_VIEW_CHANGED, because filters, order, metrics and dimensions are all bounded by the view: changing it replaces the widget rather than fixing it. Default false.
  - `dry_run` boolean: Validate and answer what WOULD happen, without writing anything. Default false.
  - `idempotency_key` string: Makes a retry safe: the answer of the first call is replayed for 24 hours to any later call with the same key and the same body.
  - `if_widget_hash` string: The widget_hash /v1/widgets/get published for this widget. The call is refused when the definition of the widget changed in between, so two writers cannot overwrite each other. A stale one is HTTP 409 WIDGET_CHANGED and details.stored_hash carries the current value; read the widget again and retry with the new hash AND THE SAME idempotency_key, which is accepted because if_widget_hash is not part of what that key identifies.

## Response `200`

The call succeeded. This endpoint reports its errors with a real HTTP status, so a 200 always carries status=ok.

- WidgetsUpdateResponse: The 200 of this write, in whichever of its shapes the call asked for.
  - `status` 'ok', required
  - `data` union, required: The answer of the write. A call sent with dry_run: true answers WriteDryRunData instead. Those two carry the preview of this operation alongside their own fields, so an answer can match more than one of these shapes.
    - WidgetsUpdateData: The widget as it was left, the backup that undoes the call, and what the rewrite cost.
      - `report_key` string, required
      - `section_key` string, required
      - `widget_key` string, required
      - `widget` ReportWidget, required: One widget as a read publishes it: what it is, where it reads from, and either its definition or the named reason it has none.
        - `widget_key` string, required
        - `name` string, nullable, required
        - `row` integer, nullable, required: 1-based row of the widget inside its section.
        - `column` integer, nullable, required: 1-based column of the widget inside its row.
        - `type` 'text' | 'ai_summary' | 'image' | 'page_break' | 'kpi' | 'metrics_summary' | 'trend' | 'chart' | 'table' | 'comparator' | 'other', required: The shape of the widget. The six data shapes and `text` are the ones the DSL can express; `ai_summary`, `image` and `page_break` carry no definition, and `other` is a shape this version does not name. A summary written by the platform used to be reported as `text`, which is the shape of the block it is stored as; it has its own value since 19 September 2026, so `text` now means a block of prose somebody wrote and nothing else.
        - `integration_id` string, nullable, required: Public id of the integration, or null when this source has no public id and cannot be addressed through the API.
        - `integration` string: Internal name of the source. Present only when integration_id is null.
        - `data_view` string, nullable, required: The level the widget reads at, in the vocabulary /v1/capabilities publishes for that integration, or null when the widget has none.
        - `connection_key` string, nullable: Present only when the report does NOT use report level data sources.
        - `account_id` string, nullable: Present only when the report does NOT use report level data sources.
        - `editable` boolean, required: Whether this widget can be expressed as a DSL definition.
        - `dsl` WidgetDsl
          - `name` string, required: Title of the widget, in the requested output language. Plain text, no markup. At most 80 characters.
          - `visualization` 'kpi' | 'graph_basic' | 'graph_pie' | 'graph_trend' | 'table_metrics_summary' | 'table_dimensions_metrics' | 'text', required: How the widget is drawn. The first six read data; text is a block of prose with no data behind it, and it is the only one that carries content.
          - `chart_type` 'area' | 'line' | 'column' | 'bar', nullable, required: Which chart a graph_basic (area, line, column or bar) or a graph_trend (area, line or column; a trend draws no bars) is drawn as. Null takes the default of the shape, which is a column chart for a graph_basic and an area chart for a graph_trend, and is the usual answer. A chart drawn along a single time dimension is built as a trend and takes the charts of a trend, so it never carries bar. The four shapes that draw no chart always carry null.
          - `full_width` boolean, nullable, required: True when the widget takes a row of its own; null, like false, pairs it up. The layout of a section is yours: the widgets you do not mark are laid out two to a row in the order you write them, and the KPIs go four to a row before them. A widget marked true closes the row being filled and the next widget opens a new one. Ignored in a kpi, which is always part of a strip of four.
          - `content` string, nullable, required: The prose of a text block, and null in every other shape. Markdown of a reduced set and nothing else: blank line between paragraphs, **bold**, lines starting with '- ' for a bullet list and with '1. ' for a numbered one. No headings, no links, no italics, no images, no HTML and no emoji; anything else is printed as the characters it is made of. At most 2000 characters. A text block carries no figures: it is written before the widgets of the report are run, so any number in it would be one nobody measured.
          - `data_view` string, required: Public id of the data view the widget reads, exactly as listed in the catalogue. Empty string when the integration has no data views, and empty string in a text block, which reads none.
          - `metrics` union[], required: Public field ids of the metrics, in the order they are shown. At least one in the six shapes that read data; empty in a text block. A column of a table_metrics_summary may also be written as the object {id, filter_key}, which reads that one column through a saved filter of the account. Set by API and MCP clients; a definition being generated always writes the field id on its own.
            - union
              - …
          - `dimensions` string[], required: Public field ids of the dimensions, in the order they are shown. Empty when the widget has none.
          - `filter_key` string, nullable, required: Key of a saved filter of the account the whole widget is read through, or null when the widget reads no such filter. It excludes filters: a widget either points at a filter of the account or carries conditions of its own, never both. The filter has to belong to the integration of the widget and, where the filters of that integration are read in a data view, to the data view of the widget. Set by API and MCP clients; a definition being generated always writes it as null.
          - `compare` object, nullable, required: What the widget is drawn several times over, or null when it compares nothing, which is the usual answer. accounts draws one series per connected account and takes at least two of them; filters draws one per saved filter of the account; breakdown splits one page metric of facebook_insights into a series per value -- the metric itself says what it is broken down by -- and only a graph_trend takes it. A comparator is only written when the widget is created: neither the class of comparator, nor whether it is broken down over time, nor the fact of being one is changed later. Which classes an integration offers depends on the entity level of its data view too. Set by API and MCP clients; a definition being generated always writes it as null.
            - `by` 'accounts' | 'filters' | 'breakdown', required: What the widget compares.
            - `items` union, required: What is compared, in the order it is drawn: one object per connected account for accounts, one per saved filter for filters, and null for a breakdown, which compares no list.
              - …
            - `over_time` 'none' | 'hour_of_day' | 'day_of_week' | 'date' | 'week' | 'month' | 'year', nullable, required: The axis a table_metrics_summary comparator is broken down along, which is what makes it an over time table. Null, like none, draws one column per compared element and is the usual answer. Only a table_metrics_summary takes it, and which axes a source offers depends on the integration and on the entity level of its data view. Switching the axis on or off is a change of subtype and is not done by an update.
            - `show_past_serie` boolean, nullable, required: True when a breakdown draws the period before as a second series. Null, like false, draws only the current period, and it is null in the other two comparators.
          - `filters` object, nullable, required: Filter applied to the widget, or null for no filter.
            - `combination` 'and' | 'or', required: How the conditions are joined. Only a combination the catalogue declares for this integration.
            - `conditions` object[], required
              - …
          - `sort_by` object, nullable, required: Ordering of the rows, or null to leave the default.
            - `field` string, required: Public field id, present in metrics or dimensions.
            - `direction` 'asc' | 'desc', required
          - `limit` integer, nullable, required: Maximum number of rows, or null for the default.
          - `date_range` object, nullable, required: Date range of the widget, or null to inherit the range of the report section.
            - `preset` string, required: One of the preset ids listed in the instructions, or custom when start and end are given.
            - `x` integer, nullable, required: Number of days, weeks or months of a preset that takes one; null otherwise.
            - `start` string, nullable, required: First day of a custom range as yyyy-MM-dd; null otherwise.
            - `end` string, nullable, required: Last day of a custom range as yyyy-MM-dd; null otherwise.
          - `previous_period` boolean, nullable, required: The comparison with the period immediately before, and each shape draws it its own way: a graph_trend draws it as an extra series, a table_dimensions_metrics as a column with the percentage of change of each metric, and a kpi and a table_metrics_summary as the figure of that period beside the current one. A few sources and views do not offer it and say so in a warning. In the two shapes that draw the figure, null means yes, which is what those widgets show by default; in the other two, null is the usual answer and asks for nothing.
          - `show_change` boolean, nullable, required: The PERCENTAGE OF CHANGE against the period before, drawn only by a kpi and by a table_metrics_summary, where it is the other half of the comparison: previous_period is the figure of that period and this one is the percentage beside it. The two are independent and neither governs the other. Null means yes, which is what those widgets show by default. The other five shapes carry null: the change asked for in one of them is ignored with a warning.
          - `settings` object[], nullable, required: Integration settings the catalogue declares, or null when none applies.
            - `id` string, required: Setting id of the catalogue.
            - `value` string, required: One of the values the catalogue lists for that setting.
          - `unfulfilled` string[], required: What was asked for and cannot be expressed with this integration, one short sentence each. Empty when everything asked for is in the widget.
        - `reason` string, nullable: Why the widget has no definition, as a stable token. Present only when editable is false.
        - `message` string, nullable: The same reason in words. Present only when editable is false.
        - `ai_summary` WidgetAiSummary: An AI summary as the body of a write says it, and as a read publishes it: the reach of the summary, what that reach reads, the settings it is generated with and the brief of this summary. The settings and the brief are a partial write: what a body does not name is left exactly as it is, a setting sent with the value the summary already uses is not written either, and a creation that names none of them is generated with the defaults /v1/capabilities publishes. It is not a visualization of the DSL, and it could not be: what a summary is made of is a selection of widgets or of sections, which is said with keys of this account's report, while a definition is portable and knows no keys. A TEMPLATE takes none: what a template holds is copied into every report built on it, and a summary reads the widgets and the sections of ONE report by key, so each copy would be a summary of nothing; it is refused with APPEND_NOT_ALLOWED and reason template_report.
          - `scope` 'widget' | 'section' | 'report', required: What the summary reads. `widget` reads the widgets of its own section it names, `section` reads the data widgets of its own section and `report` reads the sections it names. The scope of a summary that exists is never changed: a summary of another scope is placed and counted differently, so it is deleted and written again.
          - `widget_keys` string[]: Scopes `widget` and `section`: the data widgets of THIS section the summary reads. THREE STATES. Left out (or null) does not touch it: a creation falls back to what the editor preselects -- the first data widgets of the section -- and an update leaves the summary reading exactly what it reads. An EMPTY list is a value and means the summary reads nothing, which is the state the application saves a summary in with nothing ticked: it is accepted and the answer carries a SELECTION_EMPTY warning, because it generates nothing until something is selected. A list of keys replaces the selection. It is required on a CREATION of scope `widget`, which is made of them, and not on an update of one. /v1/capabilities publishes how many each scope may read.
          - `section_keys` string[]: Scope `report`: the sections of this report the summary reads. The same three states as widget_keys: left out (or null) does not touch it -- a creation reads the first sections of the report, an update keeps what the summary has -- an EMPTY list means it reads nothing and answers a SELECTION_EMPTY warning, and a list of keys replaces the selection.
          - `position` 'first' | 'last': Scopes `section` and `report`, and only when the summary is being CREATED: where it goes inside the section, alone in its row. `first`, the default, puts it above everything the section carries and moves the rest down; `last` opens a new row after the last one. A summary of scope `widget` is one more widget of its row and takes `placement` instead.
          - `ai_prompt` string, nullable: The instruction this summary is written with. A read answers the text or null, and never an empty string: an emptied box is stored as nothing, so a text that was emptied and one nobody ever wrote come back the same way. In a body, leaving the property out or sending null keeps what the summary has, an empty string empties it and a text replaces it.
          - `ai_include_comparison` boolean: Whether the summary is given the period before to compare against. True unless it was turned off.
          - `ai_auto_refresh` boolean: Whether the summary is written again on its own when the period or the data behind it changes. True unless it was turned off.
          - `ai_fallback_policy` 'omit' | 'default_text' | 'last_cached': What the end client is shown when the summary could not be produced: `omit` prints nothing, `default_text` prints ai_fallback_text and `last_cached` prints the last summary that worked, labelled with the period it belongs to. `omit` unless it was changed.
          - `ai_quota_fallback_policy` 'omit' | 'default_text' | 'last_cached': The same choice for the case where the monthly AI allowance of the account is used up, which is a different question: the data is healthy there. `omit` unless it was changed.
          - `ai_fallback_text` string, nullable: The text both policies print when they are set to `default_text`. It is never required: a policy of `default_text` with no text prints nothing, exactly as the settings window of the application saves it. A read answers the text or null and never an empty string; in a body, null keeps what the summary has and an empty string empties it.
          - `ai_show_disclaimer` boolean: Whether the summary carries the line saying it was written by AI. True unless it was turned off.
          - `ai_brief` string, nullable: What the summaries of THIS widget should know about the client. It is the narrowest of three levels and they are added together, not replaced: the level of the account is written in the application, the level of the report travels on /v1/reports/update, and this one belongs to the summary. Three states: leaving the property out -- or sending null, which is what a read answers when there is none -- keeps the brief the summary has, an empty string removes it, and a text replaces it. It is stored apart from the widget: it is not covered by widget_hash, /v1/widgets/restore does not bring it back, and a brief that could not be written comes back as an AI_BRIEF_NOT_SAVED warning of a 200.
          - `credits_per_generation` integer: What ONE generation of this summary costs the account, in credits. It is spent when the report is opened, viewed or delivered by a schedule, and never when the summary is written.
          - `credits_note` string: When those credits are spent.
        - `image` WidgetImage: What the picture of an image widget measures. The bytes themselves are never published by a read: they leave this surface drawn, inside the report.
          - `width` integer, nullable, required
          - `height` integer, nullable, required
          - `bytes` integer, nullable, required: How much the stored file weighs.
      - `widget_hash` string, nullable, required: The fingerprint of the widget after the write. Send it as if_widget_hash on the next one.
      - `backup_key` string, nullable, required: The backup taken before the write, which /v1/widgets/restore puts back. Null in a rehearsal, which writes nothing and therefore backs up nothing.
      - `lost_properties` string[], required: What the rewrite could not keep. A change of shape takes the legend position and the period headers of the new shape, and a metric that left a metrics summary takes the title its column had. /v1/capabilities lists the closed vocabulary under widgets.lost_properties.
      - `affects_only_this_report` boolean, required: True when the widget is the copy a report built on a template keeps of its own: the change reaches that report and neither the template nor the other reports built on it.
      - `unfulfilled` string[], required: What was asked for and could not be expressed; the API returns an empty list.
      - `warnings` Problem[], required: Things worth knowing about a write that DID happen. They are never errors.
        - `path` string, required: Dotted path of the offending field inside the request body, empty for a problem about the body as a whole.
        - `code` 'MISSING_REQUIRED_FIELD' | 'INVALID_VALUE' | 'UNKNOWN_PARAM' | 'INVALID_DATE_RANGE' | 'INTEGRATION_NOT_SUPPORTED' | 'CONNECTION_NOT_FOUND' | 'ACCOUNT_NOT_FOUND' | 'ONE_CONNECTION_PER_INTEGRATION' | 'DATE_PRESET_NOT_IN_REPORTS' | 'TEMPLATE_NOT_FOUND' | 'TEMPLATE_PARAMETERIZED' | 'ROW_RULE' | 'POSITION_CHANGED' | 'SECTION_CHANGED' | 'TOO_MANY_PROBLEMS' | 'DATA_SOURCES_PARTIALLY_BOUND' | 'WELCOME_EMAIL_FAILED' | 'REPORT_WILL_BE_SHARED_PUBLICLY' | 'MAY_BE_DELIVERED_AGAIN_TODAY' | 'SUMMARY_SECTION_SKIPPED' | 'COVER_FILE_REMOVED' | 'COVER_TYPE_SET' | 'LOGO_REMOVED' | 'READ_ONLY_FIELD_IGNORED' | 'AI_BRIEF_NOT_SAVED' | 'SPENDS_AI_CREDITS' | 'DROPS_ACCOUNT_CACHE' | 'CHART_TYPE_NOT_FOR_THIS_SHAPE' | 'CHART_TYPE_NOT_FOR_TREND' | 'CONTENT_NOT_FOR_THIS_SHAPE' | 'FULL_WIDTH_NOT_FOR_KPI' | 'DATA_VIEW_FIELDS_PER_ACCOUNT' | 'DATA_VIEW_NOT_AVAILABLE' | 'DATA_VIEW_NOT_EMPTY' | 'DATA_VIEW_NOT_OF_THE_SECTION' | 'DATA_VIEW_REQUIRED' | 'DATA_VIEW_UNKNOWN' | 'DATE_RANGE_CUSTOM_INCOMPLETE' | 'DATE_RANGE_CUSTOM_INVALID' | 'DATE_RANGE_CUSTOM_INVERTED' | 'DATE_RANGE_PRESET_UNKNOWN' | 'DATE_RANGE_X_REQUIRED' | 'DIMENSION_DUPLICATED' | 'DIMENSION_IS_A_METRIC' | 'DIMENSION_REQUIRED' | 'DIMENSION_UNKNOWN' | 'DSL_INVALID' | 'FIELD_NOT_IN_AD_TYPES' | 'FILTERS_NOT_SUPPORTED' | 'FILTER_COMBINATION_NOT_SUPPORTED' | 'FILTER_CONDITIONS_MIXED_KINDS' | 'FILTER_FIELD_NOT_A_COLUMN' | 'FILTER_FIELD_NOT_IN_DATA_VIEW' | 'FILTER_FIELD_UNKNOWN' | 'FILTER_LIKE_NEEDS_WILDCARD' | 'FILTER_OPERATOR_INVALID' | 'FILTER_TYPE_OF_FIELD_MISMATCH' | 'FILTER_TYPE_OF_FIELD_NOT_USED' | 'FILTER_TYPE_OF_FIELD_REQUIRED' | 'FILTER_VALUE_EMPTY' | 'FILTER_VALUE_NOT_ALLOWED' | 'FILTER_VALUE_NOT_NUMERIC' | 'GRAPH_EXTRA_DIMENSIONS' | 'GRAPH_TIME_DIMENSION_MIXED' | 'INTEGRATION_UNKNOWN' | 'INTENT_MISSING' | 'KPI_NEEDS_ONE_METRIC' | 'KPI_TAKES_NO_DIMENSIONS' | 'LIMIT_IGNORED_IN_TREND' | 'LIMIT_OUT_OF_RANGE' | 'METRICS_SUMMARY_TAKES_NO_DIMENSIONS' | 'METRIC_DUPLICATED' | 'METRIC_IS_A_DIMENSION' | 'METRIC_UNKNOWN' | 'METRICS_REQUIRED' | 'NAME_EMPTY' | 'NAME_NOT_PLAIN_TEXT' | 'NAME_TOO_LONG' | 'PIE_TAKES_ONE_DIMENSION' | 'PREVIOUS_PERIOD_NOT_AVAILABLE_FOR_THIS_TABLE' | 'COMPARISON_NOT_AVAILABLE_FOR_THIS_WIDGET' | 'PREVIOUS_PERIOD_NOT_FOR_THIS_SHAPE' | 'SHOW_CHANGE_NOT_FOR_THIS_SHAPE' | 'SHOW_CHANGE_NEEDS_PREVIOUS_PERIOD' | 'SETTING_DUPLICATED' | 'SETTING_NOT_FOR_DATA_VIEW' | 'SETTING_NOT_IN_WIDGET' | 'SETTING_UNKNOWN' | 'SETTING_VALUE_INVALID' | 'SORT_FIELD_NOT_SELECTED' | 'TABLE_TIME_DIMENSION_NOT_DRAWABLE' | 'TEXT_CONTENT_REQUIRED' | 'TEXT_CONTENT_TOO_LONG' | 'TEXT_NOT_A_WIDGET_TYPE' | 'TEXT_TAKES_NO_DATA' | 'TOO_MANY_DIMENSIONS' | 'TOO_MANY_METRICS' | 'TRANSLATION_FAILED' | 'TREND_ORDER_IS_THE_TIME_AXIS' | 'TREND_TAKES_NO_DIMENSIONS' | 'VISUALIZATION_NOT_AVAILABLE' | 'VISUALIZATION_NOT_OF_REQUESTED_TYPE' | 'FIELD_NOT_AVAILABLE' | 'FILTER_CONDITIONS_MIXED_LEVELS' | 'FILTER_LEVEL_MISMATCH' | 'FORMULA_TOKEN_UNKNOWN' | 'FORMULA_INVALID' | 'LANGUAGE_FILE_KEYS_IGNORED' | 'SELECTION_EMPTY' | 'FILTER_KEY_AND_FILTERS' | 'FILTER_KEY_NOT_FOR_THIS_SHAPE' | 'KEY_NOT_FOR_GENERATION' | 'COMPARE_NOT_FOR_THIS_SHAPE' | 'COMPARE_NOT_FOR_INTEGRATION' | 'COMPARE_ITEMS_CARDINALITY' | 'COMPARE_METRICS_CARDINALITY' | 'COMPARE_OVER_TIME_NOT_AVAILABLE' | 'COMPARE_METRIC_HAS_NO_BREAKDOWN' | 'COMPARE_KEY_NOT_FOR_THIS_COMPARATOR' | 'COMPARATOR_CLASS_FIXED', required: What is wrong with that field, or what is worth knowing about it. The first group is the vocabulary of the body itself; the eleven that follow it appear only in `warnings[]`, bar TOO_MANY_PROBLEMS, which closes a `problems` list that was truncated; and the rest are the codes of the widget DSL, which travel here when a composition or a widget carries a dsl that does not validate.
        - `message` string, required: Human-readable explanation. Not stable across versions.
        - `suggestions` string[]: The accepted values, when the field takes a closed list.
      - `ai_summary` WidgetAiSummary: An AI summary as the body of a write says it, and as a read publishes it: the reach of the summary, what that reach reads, the settings it is generated with and the brief of this summary. The settings and the brief are a partial write: what a body does not name is left exactly as it is, a setting sent with the value the summary already uses is not written either, and a creation that names none of them is generated with the defaults /v1/capabilities publishes. It is not a visualization of the DSL, and it could not be: what a summary is made of is a selection of widgets or of sections, which is said with keys of this account's report, while a definition is portable and knows no keys. A TEMPLATE takes none: what a template holds is copied into every report built on it, and a summary reads the widgets and the sections of ONE report by key, so each copy would be a summary of nothing; it is refused with APPEND_NOT_ALLOWED and reason template_report.
        - `scope` 'widget' | 'section' | 'report', required: What the summary reads. `widget` reads the widgets of its own section it names, `section` reads the data widgets of its own section and `report` reads the sections it names. The scope of a summary that exists is never changed: a summary of another scope is placed and counted differently, so it is deleted and written again.
        - `widget_keys` string[]: Scopes `widget` and `section`: the data widgets of THIS section the summary reads. THREE STATES. Left out (or null) does not touch it: a creation falls back to what the editor preselects -- the first data widgets of the section -- and an update leaves the summary reading exactly what it reads. An EMPTY list is a value and means the summary reads nothing, which is the state the application saves a summary in with nothing ticked: it is accepted and the answer carries a SELECTION_EMPTY warning, because it generates nothing until something is selected. A list of keys replaces the selection. It is required on a CREATION of scope `widget`, which is made of them, and not on an update of one. /v1/capabilities publishes how many each scope may read.
        - `section_keys` string[]: Scope `report`: the sections of this report the summary reads. The same three states as widget_keys: left out (or null) does not touch it -- a creation reads the first sections of the report, an update keeps what the summary has -- an EMPTY list means it reads nothing and answers a SELECTION_EMPTY warning, and a list of keys replaces the selection.
        - `position` 'first' | 'last': Scopes `section` and `report`, and only when the summary is being CREATED: where it goes inside the section, alone in its row. `first`, the default, puts it above everything the section carries and moves the rest down; `last` opens a new row after the last one. A summary of scope `widget` is one more widget of its row and takes `placement` instead.
        - `ai_prompt` string, nullable: The instruction this summary is written with. A read answers the text or null, and never an empty string: an emptied box is stored as nothing, so a text that was emptied and one nobody ever wrote come back the same way. In a body, leaving the property out or sending null keeps what the summary has, an empty string empties it and a text replaces it.
        - `ai_include_comparison` boolean: Whether the summary is given the period before to compare against. True unless it was turned off.
        - `ai_auto_refresh` boolean: Whether the summary is written again on its own when the period or the data behind it changes. True unless it was turned off.
        - `ai_fallback_policy` 'omit' | 'default_text' | 'last_cached': What the end client is shown when the summary could not be produced: `omit` prints nothing, `default_text` prints ai_fallback_text and `last_cached` prints the last summary that worked, labelled with the period it belongs to. `omit` unless it was changed.
        - `ai_quota_fallback_policy` 'omit' | 'default_text' | 'last_cached': The same choice for the case where the monthly AI allowance of the account is used up, which is a different question: the data is healthy there. `omit` unless it was changed.
        - `ai_fallback_text` string, nullable: The text both policies print when they are set to `default_text`. It is never required: a policy of `default_text` with no text prints nothing, exactly as the settings window of the application saves it. A read answers the text or null and never an empty string; in a body, null keeps what the summary has and an empty string empties it.
        - `ai_show_disclaimer` boolean: Whether the summary carries the line saying it was written by AI. True unless it was turned off.
        - `ai_brief` string, nullable: What the summaries of THIS widget should know about the client. It is the narrowest of three levels and they are added together, not replaced: the level of the account is written in the application, the level of the report travels on /v1/reports/update, and this one belongs to the summary. Three states: leaving the property out -- or sending null, which is what a read answers when there is none -- keeps the brief the summary has, an empty string removes it, and a text replaces it. It is stored apart from the widget: it is not covered by widget_hash, /v1/widgets/restore does not bring it back, and a brief that could not be written comes back as an AI_BRIEF_NOT_SAVED warning of a 200.
        - `credits_per_generation` integer: What ONE generation of this summary costs the account, in credits. It is spent when the report is opened, viewed or delivered by a schedule, and never when the summary is written.
        - `credits_note` string: When those credits are spent.
      - `before` WidgetAiSummary: An AI summary as the body of a write says it, and as a read publishes it: the reach of the summary, what that reach reads, the settings it is generated with and the brief of this summary. The settings and the brief are a partial write: what a body does not name is left exactly as it is, a setting sent with the value the summary already uses is not written either, and a creation that names none of them is generated with the defaults /v1/capabilities publishes. It is not a visualization of the DSL, and it could not be: what a summary is made of is a selection of widgets or of sections, which is said with keys of this account's report, while a definition is portable and knows no keys. A TEMPLATE takes none: what a template holds is copied into every report built on it, and a summary reads the widgets and the sections of ONE report by key, so each copy would be a summary of nothing; it is refused with APPEND_NOT_ALLOWED and reason template_report.
        - `scope` 'widget' | 'section' | 'report', required: What the summary reads. `widget` reads the widgets of its own section it names, `section` reads the data widgets of its own section and `report` reads the sections it names. The scope of a summary that exists is never changed: a summary of another scope is placed and counted differently, so it is deleted and written again.
        - `widget_keys` string[]: Scopes `widget` and `section`: the data widgets of THIS section the summary reads. THREE STATES. Left out (or null) does not touch it: a creation falls back to what the editor preselects -- the first data widgets of the section -- and an update leaves the summary reading exactly what it reads. An EMPTY list is a value and means the summary reads nothing, which is the state the application saves a summary in with nothing ticked: it is accepted and the answer carries a SELECTION_EMPTY warning, because it generates nothing until something is selected. A list of keys replaces the selection. It is required on a CREATION of scope `widget`, which is made of them, and not on an update of one. /v1/capabilities publishes how many each scope may read.
        - `section_keys` string[]: Scope `report`: the sections of this report the summary reads. The same three states as widget_keys: left out (or null) does not touch it -- a creation reads the first sections of the report, an update keeps what the summary has -- an EMPTY list means it reads nothing and answers a SELECTION_EMPTY warning, and a list of keys replaces the selection.
        - `position` 'first' | 'last': Scopes `section` and `report`, and only when the summary is being CREATED: where it goes inside the section, alone in its row. `first`, the default, puts it above everything the section carries and moves the rest down; `last` opens a new row after the last one. A summary of scope `widget` is one more widget of its row and takes `placement` instead.
        - `ai_prompt` string, nullable: The instruction this summary is written with. A read answers the text or null, and never an empty string: an emptied box is stored as nothing, so a text that was emptied and one nobody ever wrote come back the same way. In a body, leaving the property out or sending null keeps what the summary has, an empty string empties it and a text replaces it.
        - `ai_include_comparison` boolean: Whether the summary is given the period before to compare against. True unless it was turned off.
        - `ai_auto_refresh` boolean: Whether the summary is written again on its own when the period or the data behind it changes. True unless it was turned off.
        - `ai_fallback_policy` 'omit' | 'default_text' | 'last_cached': What the end client is shown when the summary could not be produced: `omit` prints nothing, `default_text` prints ai_fallback_text and `last_cached` prints the last summary that worked, labelled with the period it belongs to. `omit` unless it was changed.
        - `ai_quota_fallback_policy` 'omit' | 'default_text' | 'last_cached': The same choice for the case where the monthly AI allowance of the account is used up, which is a different question: the data is healthy there. `omit` unless it was changed.
        - `ai_fallback_text` string, nullable: The text both policies print when they are set to `default_text`. It is never required: a policy of `default_text` with no text prints nothing, exactly as the settings window of the application saves it. A read answers the text or null and never an empty string; in a body, null keeps what the summary has and an empty string empties it.
        - `ai_show_disclaimer` boolean: Whether the summary carries the line saying it was written by AI. True unless it was turned off.
        - `ai_brief` string, nullable: What the summaries of THIS widget should know about the client. It is the narrowest of three levels and they are added together, not replaced: the level of the account is written in the application, the level of the report travels on /v1/reports/update, and this one belongs to the summary. Three states: leaving the property out -- or sending null, which is what a read answers when there is none -- keeps the brief the summary has, an empty string removes it, and a text replaces it. It is stored apart from the widget: it is not covered by widget_hash, /v1/widgets/restore does not bring it back, and a brief that could not be written comes back as an AI_BRIEF_NOT_SAVED warning of a 200.
        - `credits_per_generation` integer: What ONE generation of this summary costs the account, in credits. It is spent when the report is opened, viewed or delivered by a schedule, and never when the summary is written.
        - `credits_note` string: When those credits are spent.
    - WriteDryRunData: The answer of a call sent with dry_run: true. Nothing was written, no idempotency key was consumed and no confirmation token was burnt. When valid is true, the fields of the endpoint's own preview travel alongside these two; when it is false, `message` and `problems` do instead. The HTTP status is 200 in both cases: a rehearsal that says the body is wrong is a successful rehearsal. Two refusals are NOT translated and arrive with their real status: a 429 (the budgets are spent either way) and any 5xx.
      - `dry_run` true, required: Always true in this shape.
      - `valid` boolean, required: Whether the body would be accepted.
      - `message` string: Why the body would be refused. Present only when valid is false.
      - `problems` Problem[]: Everything that is wrong with the body. Present only when valid is false.
        - `path` string, required: Dotted path of the offending field inside the request body, empty for a problem about the body as a whole.
        - `code` 'MISSING_REQUIRED_FIELD' | 'INVALID_VALUE' | 'UNKNOWN_PARAM' | 'INVALID_DATE_RANGE' | 'INTEGRATION_NOT_SUPPORTED' | 'CONNECTION_NOT_FOUND' | 'ACCOUNT_NOT_FOUND' | 'ONE_CONNECTION_PER_INTEGRATION' | 'DATE_PRESET_NOT_IN_REPORTS' | 'TEMPLATE_NOT_FOUND' | 'TEMPLATE_PARAMETERIZED' | 'ROW_RULE' | 'POSITION_CHANGED' | 'SECTION_CHANGED' | 'TOO_MANY_PROBLEMS' | 'DATA_SOURCES_PARTIALLY_BOUND' | 'WELCOME_EMAIL_FAILED' | 'REPORT_WILL_BE_SHARED_PUBLICLY' | 'MAY_BE_DELIVERED_AGAIN_TODAY' | 'SUMMARY_SECTION_SKIPPED' | 'COVER_FILE_REMOVED' | 'COVER_TYPE_SET' | 'LOGO_REMOVED' | 'READ_ONLY_FIELD_IGNORED' | 'AI_BRIEF_NOT_SAVED' | 'SPENDS_AI_CREDITS' | 'DROPS_ACCOUNT_CACHE' | 'CHART_TYPE_NOT_FOR_THIS_SHAPE' | 'CHART_TYPE_NOT_FOR_TREND' | 'CONTENT_NOT_FOR_THIS_SHAPE' | 'FULL_WIDTH_NOT_FOR_KPI' | 'DATA_VIEW_FIELDS_PER_ACCOUNT' | 'DATA_VIEW_NOT_AVAILABLE' | 'DATA_VIEW_NOT_EMPTY' | 'DATA_VIEW_NOT_OF_THE_SECTION' | 'DATA_VIEW_REQUIRED' | 'DATA_VIEW_UNKNOWN' | 'DATE_RANGE_CUSTOM_INCOMPLETE' | 'DATE_RANGE_CUSTOM_INVALID' | 'DATE_RANGE_CUSTOM_INVERTED' | 'DATE_RANGE_PRESET_UNKNOWN' | 'DATE_RANGE_X_REQUIRED' | 'DIMENSION_DUPLICATED' | 'DIMENSION_IS_A_METRIC' | 'DIMENSION_REQUIRED' | 'DIMENSION_UNKNOWN' | 'DSL_INVALID' | 'FIELD_NOT_IN_AD_TYPES' | 'FILTERS_NOT_SUPPORTED' | 'FILTER_COMBINATION_NOT_SUPPORTED' | 'FILTER_CONDITIONS_MIXED_KINDS' | 'FILTER_FIELD_NOT_A_COLUMN' | 'FILTER_FIELD_NOT_IN_DATA_VIEW' | 'FILTER_FIELD_UNKNOWN' | 'FILTER_LIKE_NEEDS_WILDCARD' | 'FILTER_OPERATOR_INVALID' | 'FILTER_TYPE_OF_FIELD_MISMATCH' | 'FILTER_TYPE_OF_FIELD_NOT_USED' | 'FILTER_TYPE_OF_FIELD_REQUIRED' | 'FILTER_VALUE_EMPTY' | 'FILTER_VALUE_NOT_ALLOWED' | 'FILTER_VALUE_NOT_NUMERIC' | 'GRAPH_EXTRA_DIMENSIONS' | 'GRAPH_TIME_DIMENSION_MIXED' | 'INTEGRATION_UNKNOWN' | 'INTENT_MISSING' | 'KPI_NEEDS_ONE_METRIC' | 'KPI_TAKES_NO_DIMENSIONS' | 'LIMIT_IGNORED_IN_TREND' | 'LIMIT_OUT_OF_RANGE' | 'METRICS_SUMMARY_TAKES_NO_DIMENSIONS' | 'METRIC_DUPLICATED' | 'METRIC_IS_A_DIMENSION' | 'METRIC_UNKNOWN' | 'METRICS_REQUIRED' | 'NAME_EMPTY' | 'NAME_NOT_PLAIN_TEXT' | 'NAME_TOO_LONG' | 'PIE_TAKES_ONE_DIMENSION' | 'PREVIOUS_PERIOD_NOT_AVAILABLE_FOR_THIS_TABLE' | 'COMPARISON_NOT_AVAILABLE_FOR_THIS_WIDGET' | 'PREVIOUS_PERIOD_NOT_FOR_THIS_SHAPE' | 'SHOW_CHANGE_NOT_FOR_THIS_SHAPE' | 'SHOW_CHANGE_NEEDS_PREVIOUS_PERIOD' | 'SETTING_DUPLICATED' | 'SETTING_NOT_FOR_DATA_VIEW' | 'SETTING_NOT_IN_WIDGET' | 'SETTING_UNKNOWN' | 'SETTING_VALUE_INVALID' | 'SORT_FIELD_NOT_SELECTED' | 'TABLE_TIME_DIMENSION_NOT_DRAWABLE' | 'TEXT_CONTENT_REQUIRED' | 'TEXT_CONTENT_TOO_LONG' | 'TEXT_NOT_A_WIDGET_TYPE' | 'TEXT_TAKES_NO_DATA' | 'TOO_MANY_DIMENSIONS' | 'TOO_MANY_METRICS' | 'TRANSLATION_FAILED' | 'TREND_ORDER_IS_THE_TIME_AXIS' | 'TREND_TAKES_NO_DIMENSIONS' | 'VISUALIZATION_NOT_AVAILABLE' | 'VISUALIZATION_NOT_OF_REQUESTED_TYPE' | 'FIELD_NOT_AVAILABLE' | 'FILTER_CONDITIONS_MIXED_LEVELS' | 'FILTER_LEVEL_MISMATCH' | 'FORMULA_TOKEN_UNKNOWN' | 'FORMULA_INVALID' | 'LANGUAGE_FILE_KEYS_IGNORED' | 'SELECTION_EMPTY' | 'FILTER_KEY_AND_FILTERS' | 'FILTER_KEY_NOT_FOR_THIS_SHAPE' | 'KEY_NOT_FOR_GENERATION' | 'COMPARE_NOT_FOR_THIS_SHAPE' | 'COMPARE_NOT_FOR_INTEGRATION' | 'COMPARE_ITEMS_CARDINALITY' | 'COMPARE_METRICS_CARDINALITY' | 'COMPARE_OVER_TIME_NOT_AVAILABLE' | 'COMPARE_METRIC_HAS_NO_BREAKDOWN' | 'COMPARE_KEY_NOT_FOR_THIS_COMPARATOR' | 'COMPARATOR_CLASS_FIXED', required: What is wrong with that field, or what is worth knowing about it. The first group is the vocabulary of the body itself; the eleven that follow it appear only in `warnings[]`, bar TOO_MANY_PROBLEMS, which closes a `problems` list that was truncated; and the rest are the codes of the widget DSL, which travel here when a composition or a widget carries a dsl that does not validate.
        - `message` string, required: Human-readable explanation. Not stable across versions.
        - `suggestions` string[]: The accepted values, when the field takes a closed list.
  - `meta` WriteMeta, required: The meta of a write answer. A replay carries two extra fields so it can be told apart from a first call and still traced back to it.
    - `request_id` string, required: Id of THIS request, including on a replay.
    - `timestamp` string, date-time, required
    - `replayed` boolean: True when this answer is the stored answer of an earlier call with the same idempotency_key and the same body. Absent on a first call.
    - `replayed_from_request_id` string, nullable: The request_id of the call that actually did the work. Present only on a replay, and null when the stored answer carried no id.

## Other responses

- `400`: The request was refused before anything was written. INVALID_REQUEST covers the framework refusals of every endpoint (the Content-Type is not application/json, the body could not be read, the body is not valid JSON). The report endpoints add their own: VALIDATION_FAILED with details.problems, UNKNOWN_PARAM with the path of the field the endpoint does not read, and the single-field refusals MISSING_REQUIRED_FIELD, INVALID_VALUE and APPEND_NOT_ALLOWED. The description of each operation lists the ones it emits. Everything else a body can get wrong arrives as an entry of details.problems rather than as the code of the envelope.
- `401`: Invalid or missing Bearer token.
- `403`: The account may not perform this write. WRITE_ACCESS_DISABLED means write access has not been enabled for the API key in the REST API tab of the app. PLAN_LIMIT_REACHED means the plan has no room left for another report or template, and is emitted only by the endpoints that create one.
- `404`: A key of the request does not name anything this account owns. NOT_FOUND covers reports, sections, widgets, schedules, portal views, brand templates, email templates and language files; TEMPLATE_NOT_FOUND covers a catalog slug, version or section position that stopped being published between the validation and the write. An entity of another account is reported the same way as one that does not exist.
- `409`: The request is well formed and the current state does not allow it. Every write can answer IDEMPOTENCY_CONFLICT (the same idempotency_key with a different body) and IDEMPOTENCY_IN_PROGRESS (an identical call is running, with a Retry-After). The endpoints that ask for a confirmation add CONFIRMATION_EXPIRED and CONFIRMATION_MISMATCH; the ones that never confirm cannot answer either. Each operation lists the domain conflicts it adds on top of those.
- `413`: The request body is over 1 MB. The cap is applied before authentication, so this answer is refused without the key being read, and it carries the usual error envelope.
- `429`: A budget was spent. RATE_LIMITED is the per-key read budget (300/minute, 5000/day) or the per-account write budget (60/minute, 1000/day). EMAIL_QUOTA_REACHED is the separate budget of 50 calls per account per day that send email, and its Retry-After points at the next UTC midnight. The Retry-After header gives the seconds until the more restrictive window resets. The X-RateLimit-* headers report the read budget, except on the 429 of a write, where they report the write budget.
- `500`: Internal server error.
- `503`: Writes are paused, or the store the write guarantees rely on is not answering. Nothing was written and the call is worth retrying later. Reads are unaffected.

---

[API](https://skmtc.dev/reportingninja/apis/reporting-ninja-api.md) · [All operations](https://skmtc.dev/reportingninja/apis/reporting-ninja-api/llms.txt) · [OpenAPI document](https://skmtc.dev/reportingninja/apis/reporting-ninja-api/revisions/13876c71be34?raw)
