---
title: "Create a report from a composition"
method: POST
path: "/reports/create"
tags: ["Reports"]
---

# Create a report from a composition

`POST /reports/create`

## Request

- Base URL: `https://api.reportingninja.com/v1`
- URL: `https://api.reportingninja.com/v1/reports/create`
- Auth: HTTP bearer

Writes a report (or one of the account's own templates) out of a composition: sections copied from the published catalog and/or sections written from scratch whose widgets are given as DSL, bound to the account's connections through data_sources. Follow the discovery chain first: /v1/connections for the bindings, /v1/fields and /v1/integrations/detail for the field ids and data views of every widget, /v1/capabilities for the ceilings and the presets. A section is either `catalog` -- a section of a published entry, named by its slug and its 0-based position -- or `custom`, whose widgets are written as DSL. This is NOT the vocabulary of the AI assistant of the app: nothing here is natural language. Every property of the dsl schema is required, `unfulfilled` included: send null in the optional ones and an empty array in `unfulfilled`. That field belongs to the AI generator, which lists there what it could not express; a client writing the DSL itself sends an empty array. The `data_view` of a dsl takes its values from /v1/capabilities (the integrations block), which is not always the data_views list of /v1/integrations/detail: an integration that reads a single level when it is queried can still be drawn at several levels in a report. A refusal names the accepted values in the problem's `suggestions`. A widget of a custom section whose dsl has `visualization: text` is a block of prose: it reads no data, so it carries no integration_id, the empty value of every data key and its Markdown in `content`. Every section, catalog or custom, carries `add_summary` and `show_previous_period`, both required and both usually null: `add_summary` writes an AI summary of that section -- it does not count against the widgets of the section and it spends AI credits of the account when the report is opened, viewed or delivered by a schedule, never when it is written -- and `show_previous_period` makes its header name the period it is compared against. The `add_summary` at the root of the body is another thing -- the summary of the WHOLE report -- and it spends AI credits of the account and is off by default. A section summary that cannot be written comes back as a SUMMARY_SECTION_SKIPPED warning with the path of the section, and the rest of the report is written. This endpoint never asks for a confirmation and does not accept if_version. Refusals: 400 VALIDATION_FAILED, whose details.problems name what is wrong with each field -- a catalog slug or position that is not published (TEMPLATE_NOT_FOUND), a parameterized entry used as a section (TEMPLATE_PARAMETERIZED), an integration a section reads from with no data source (MISSING_REQUIRED_FIELD), two entries naming the same integration (ONE_CONNECTION_PER_INTEGRATION), a dsl that does not validate (DSL_INVALID and the codes of the widget DSL) -- or 400 UNKNOWN_PARAM; 403 WRITE_ACCESS_DISABLED when write access is off and PLAN_LIMIT_REACHED when the plan has no room for another report; 404 TEMPLATE_NOT_FOUND only when a catalog section stops being published between the validation and the write; 409 for the idempotency conflicts.

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

## Request body

- ReportsCreateRequest: A composition: what to call the report, which connections it reads from and which sections it is made of.
  - `name` string, required: The title of the report, 1 to 60 characters: the name the cover paints. It is what /v1/reports/get answers as `name`.
  - `internal_name` string: The name the list of the application shows, 1 to 100 characters. Left out, it is the title.
  - `type` 'report' | 'template': What to create. Default report.
  - `language` 'en' | 'es': Language of the report's own literals. Defaults to the account owner's.
  - `date_range` CompositionDateRange: The period a report is built for. Its vocabulary is not the one of /v1/query: the presets a report has no equivalent for (lastweeksunsat, lastweekmonsun, thismonthtoyesterday, thisyear, lastyear, lastyeartodate, lastxweeksiso, lastxyears) are refused with the problem code DATE_PRESET_NOT_IN_REPORTS and the list of the ones that do exist.
    - `preset` 'today' | 'yesterday' | 'lastweek' | 'thisweek' | 'lastmonth' | 'thismonth' | 'lastxdays' | 'lastxweeks' | 'lastxmonths' | 'yeartolastmonth' | 'yeartolastweek' | 'custom', required: The period of the report.
    - `x` integer: How many days, weeks or months. Required by the lastx* presets.
    - `start` string: First day of a custom period, as yyyy-MM-dd.
    - `end` string: Last day of a custom period, as yyyy-MM-dd.
  - `add_summary` union: Ask the AI for a report level summary. It SPENDS AI credits of the account and is false by default. An object puts it in its own section or at the top of the first one.
    - boolean
    - object
      - `placement` 'own_section' | 'first_section'
  - `data_sources` DataSourceInput[]: The report level bindings. One entry per integration at most; every integration a section reads from needs one.
    - `integration_id` string, required: Which integration this binding is for. Two entries may not name the same one.
    - `connection_key` string, required: One of the connection keys of /v1/connections.
    - `account_id` string, required: One of the account ids of that connection.
    - `entity` 'campaign' | 'list': REQUIRED for mailchimp and rejected for every other integration: which of its two levels this binding is for.
  - `sections` SectionInput[], required: Between 1 and 12 sections, 60 widgets in total at most. A section is either `catalog` -- a section of a published entry, named by its slug and its 0-based position -- or `custom`, whose widgets are written as DSL. This is NOT the vocabulary of the AI assistant of the app: nothing here is natural language.
    - union: One section of a composition. A section is either `catalog` -- a section of a published entry, named by its slug and its 0-based position -- or `custom`, whose widgets are written as DSL. This is NOT the vocabulary of the AI assistant of the app: nothing here is natural language.
      - CatalogSectionInput: A section copied from a published catalog entry. The version is not sent: the version published today is always the one taken.
        - `type` 'catalog', required
        - `slug` string, required: The catalog entry the section is copied from.
        - `position` integer, required: 0-based position of the section inside that entry.
        - `add_summary` union, required: An AI summary OF THIS SECTION, written as one more widget of it and alone in its row. Required in every section, and null is the usual value: ask for one only when the request does. `true` is the short way of writing the first row of the section, above everything it carries; an object names the position. It is accepted when sections are appended as well, unlike the summary of the whole report. It does NOT count against the 12 widgets of the section: the platform writes it, like the summary of the report. It spends AI credits of the account WHEN THE REPORT IS OPENED, VIEWED OR DELIVERED BY A SCHEDULE, never when it is written, and the price is in `composition.section_summary` of /v1/capabilities. A section that cannot hold one -- it carries no KPI, chart or table for the summary to read, the report summary opens it, a catalog section brings a summary of its own, or the report is at its ceiling of AI widgets -- is written without it and the answer carries a SUMMARY_SECTION_SKIPPED warning naming the section. That decision is taken while the report is written, so a dry run never carries the warning: it answers `would_create.section_summaries`, which counts the ones ASKED FOR.
          - boolean
          - object
            - `position` 'first' | 'last', required: `first` puts the summary in the first row of the section and moves everything else down; `last` opens a new row after the last one.
        - `show_previous_period` boolean, nullable, required: Required, and null is the usual value. True makes the header of the section name the period it is compared against, under its date range, and it also shows that date range when the section hid it, because the one is read under the other; null, like false, leaves the header as it is.
      - CustomSectionInput: A section written from scratch, with the widgets the request defines.
        - `type` 'custom', required
        - `name` string, required: Title of the section, between 1 and 80 characters.
        - `widgets` WidgetInput[], required: At most 12 widgets per section.
          - `integration_id` string: Which integration the widget reads from. It must have a data source in the report. Required for every widget except one whose dsl has `visualization: text`, which reads no data: OMIT the property for a text block, do not send null. Sending a real integration there is accepted and checked the same way, and never read.
          - `dsl` WidgetDsl, required
            - `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.
              - …
            - `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.
              - …
            - `filters` object, nullable, required: Filter applied to the widget, or null for no filter.
              - …
            - `sort_by` object, nullable, required: Ordering of the rows, or null to leave the default.
              - …
            - `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.
              - …
            - `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.
              - …
            - `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.
        - `add_summary` union, required: An AI summary OF THIS SECTION, written as one more widget of it and alone in its row. Required in every section, and null is the usual value: ask for one only when the request does. `true` is the short way of writing the first row of the section, above everything it carries; an object names the position. It is accepted when sections are appended as well, unlike the summary of the whole report. It does NOT count against the 12 widgets of the section: the platform writes it, like the summary of the report. It spends AI credits of the account WHEN THE REPORT IS OPENED, VIEWED OR DELIVERED BY A SCHEDULE, never when it is written, and the price is in `composition.section_summary` of /v1/capabilities. A section that cannot hold one -- it carries no KPI, chart or table for the summary to read, the report summary opens it, a catalog section brings a summary of its own, or the report is at its ceiling of AI widgets -- is written without it and the answer carries a SUMMARY_SECTION_SKIPPED warning naming the section. That decision is taken while the report is written, so a dry run never carries the warning: it answers `would_create.section_summaries`, which counts the ones ASKED FOR.
          - boolean
          - object
            - `position` 'first' | 'last', required: `first` puts the summary in the first row of the section and moves everything else down; `last` opens a new row after the last one.
        - `show_previous_period` boolean, nullable, required: Required, and null is the usual value. True makes the header of the section name the period it is compared against, under its date range, and it also shows that date range when the section hid it, because the one is read under the other; null, like false, leaves the header as it is.
  - `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.

## Response `200`

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

- ReportsCreateResponse: 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.
    - ReportsCreateData: The report that was created, with the key of every section and widget it now holds.
      - `report_key` string, required
      - `app_url` string, nullable, required: The link that opens the report in the Reporting Ninja application, the same one the reads publish. Opening it requires a signed-in user of the account; it is not the public link, which is `share_url` and exists only once the report has been shared.
      - `type` 'report' | 'template' | 'template_based', required
      - `name` string, nullable, required
      - `version` number, required: The version its sections structure starts at.
      - `created_via` 'api', required
      - `sections` WrittenSection[], required
        - `section_key` string, required
        - `name` string, nullable, required
        - `origin` 'catalog' | 'custom' | 'summary', required: Where the section came from: copied from the catalog, written from the widgets of the request, or the AI summary section.
        - `widgets` WrittenWidget[], required
          - `widget_key` string, required
          - `name` string, nullable, required
          - `row` integer, nullable, required
          - `column` integer, nullable, required
          - `integration_id` string, nullable: 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.
      - `created_keys` string[], required: Every key this call created, flat, in the order they were written.
      - `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.
    - 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)
